Changeset View
Changeset View
Standalone View
Standalone View
cashtab/src/components/__tests__/App.test.js
// Copyright (c) 2024 The Bitcoin developers | // Copyright (c) 2024 The Bitcoin developers | ||||
// Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
import React from 'react'; | import React from 'react'; | ||||
import { render, screen, waitFor } from '@testing-library/react'; | import { render, screen, waitFor } from '@testing-library/react'; | ||||
import userEvent, { | import userEvent, { | ||||
PointerEventsCheckLevel, | PointerEventsCheckLevel, | ||||
} from '@testing-library/user-event'; | } from '@testing-library/user-event'; | ||||
import '@testing-library/jest-dom'; | import '@testing-library/jest-dom'; | ||||
import { | import { | ||||
walletWithXecAndTokens, | walletWithXecAndTokens, | ||||
walletWithXecAndTokens_pre_2_1_0, | walletWithXecAndTokens_pre_2_1_0, | ||||
walletWithXecAndTokens_pre_2_9_0, | |||||
freshWalletWithOneIncomingCashtabMsg, | freshWalletWithOneIncomingCashtabMsg, | ||||
requiredUtxoThisToken, | requiredUtxoThisToken, | ||||
easterEggTokenChronikTokenDetails, | easterEggTokenChronikTokenDetails, | ||||
vipTokenChronikTokenDetails, | vipTokenChronikTokenMocks, | ||||
validSavedWallets_pre_2_1_0, | validSavedWallets_pre_2_1_0, | ||||
validSavedWallets_pre_2_9_0, | |||||
validSavedWallets, | validSavedWallets, | ||||
mockCacheWalletWithXecAndTokens, | |||||
} from 'components/fixtures/mocks'; | } from 'components/fixtures/mocks'; | ||||
import 'fake-indexeddb/auto'; | import 'fake-indexeddb/auto'; | ||||
import localforage from 'localforage'; | import localforage from 'localforage'; | ||||
import { when } from 'jest-when'; | import { when } from 'jest-when'; | ||||
import appConfig from 'config/app'; | import appConfig from 'config/app'; | ||||
import { | import { | ||||
clearLocalForage, | clearLocalForage, | ||||
initializeCashtabStateForTests, | initializeCashtabStateForTests, | ||||
initializeCashtabStateAtLegacyWalletKeysForTests, | initializeCashtabStateAtLegacyWalletKeysForTests, | ||||
prepareMockedChronikCallsForWallet, | prepareMockedChronikCallsForWallet, | ||||
} from 'components/fixtures/helpers'; | } from 'components/fixtures/helpers'; | ||||
import CashtabTestWrapper from 'components/fixtures/CashtabTestWrapper'; | import CashtabTestWrapper from 'components/fixtures/CashtabTestWrapper'; | ||||
import { explorer } from 'config/explorer'; | import { explorer } from 'config/explorer'; | ||||
import { legacyMockTokenInfoById } from 'chronik/fixtures/chronikUtxos'; | import { legacyMockTokenInfoById } from 'chronik/fixtures/chronikUtxos'; | ||||
import { cashtabCacheToJSON } from 'helpers'; | import { | ||||
cashtabCacheToJSON, | |||||
storedCashtabCacheToMap, | |||||
cashtabWalletFromJSON, | |||||
cashtabWalletsFromJSON, | |||||
} from 'helpers'; | |||||
import { createCashtabWallet } from 'wallet'; | import { createCashtabWallet } from 'wallet'; | ||||
import { isValidCashtabWallet } from 'validation'; | import { isValidCashtabWallet } from 'validation'; | ||||
import CashtabCache from 'config/CashtabCache'; | |||||
// https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function | // https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function | ||||
Object.defineProperty(window, 'matchMedia', { | Object.defineProperty(window, 'matchMedia', { | ||||
writable: true, | writable: true, | ||||
value: jest.fn().mockImplementation(query => ({ | value: jest.fn().mockImplementation(query => ({ | ||||
matches: false, | matches: false, | ||||
media: query, | media: query, | ||||
onchange: null, | onchange: null, | ||||
▲ Show 20 Lines • Show All 556 Lines • ▼ Show 20 Lines | it('Setting "ABSOLUTE MINIMUM fees" settings will reduce fees to absolute min', async () => { | ||||
const mockedChronik = await initializeCashtabStateForTests( | const mockedChronik = await initializeCashtabStateForTests( | ||||
walletWithVipToken, | walletWithVipToken, | ||||
localforage, | localforage, | ||||
); | ); | ||||
// Make sure the app can get this token's genesis info by calling a mock | // Make sure the app can get this token's genesis info by calling a mock | ||||
mockedChronik.setMock('token', { | mockedChronik.setMock('token', { | ||||
input: appConfig.vipSettingsTokenId, | input: appConfig.vipSettingsTokenId, | ||||
output: vipTokenChronikTokenDetails, | output: vipTokenChronikTokenMocks.token, | ||||
}); | |||||
mockedChronik.setMock('tx', { | |||||
input: appConfig.vipSettingsTokenId, | |||||
output: vipTokenChronikTokenMocks.tx, | |||||
}); | }); | ||||
// Can verify in Electrum that this tx is sent at 1.0 sat/byte | // Can verify in Electrum that this tx is sent at 1.0 sat/byte | ||||
const hex = | const hex = | ||||
'0200000001fe667fba52a1aa603a892126e492717eed3dad43bfea7365a7fdd08e051e8a21020000006a473044022043679b2fcde0099b0cd29bfbca382e92e3b871c079a0db7d73c39440d067f5bb02202e2ab2d5d83b70911da2758afd9e56eaaaa989050f35e4cc4d28d20afc29778a4121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffff027c150000000000001976a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88acb26d0e00000000001976a9143a5fb236934ec078b4507c303d3afd82067f8fc188ac00000000'; | '0200000001fe667fba52a1aa603a892126e492717eed3dad43bfea7365a7fdd08e051e8a21020000006a473044022043679b2fcde0099b0cd29bfbca382e92e3b871c079a0db7d73c39440d067f5bb02202e2ab2d5d83b70911da2758afd9e56eaaaa989050f35e4cc4d28d20afc29778a4121031d4603bdc23aca9432f903e3cf5975a3f655cc3fa5057c61d00dfc1ca5dfd02dffffffff027c150000000000001976a9146ffbe7c7d7bd01295eb1e371de9550339bdcf9fd88acb26d0e00000000001976a9143a5fb236934ec078b4507c303d3afd82067f8fc188ac00000000'; | ||||
const txid = | const txid = | ||||
'6d2e157e2e2b1fa47cc63ede548375213942e29c090f5d9cbc2722258f720c08'; | '6d2e157e2e2b1fa47cc63ede548375213942e29c090f5d9cbc2722258f720c08'; | ||||
mockedChronik.setMock('broadcastTx', { | mockedChronik.setMock('broadcastTx', { | ||||
▲ Show 20 Lines • Show All 114 Lines • ▼ Show 20 Lines | it('Wallet with easter egg token sees easter egg', async () => { | ||||
const requiredEasterEggUtxo = { | const requiredEasterEggUtxo = { | ||||
...requiredUtxoThisToken, | ...requiredUtxoThisToken, | ||||
token: { | token: { | ||||
...requiredUtxoThisToken.token, | ...requiredUtxoThisToken.token, | ||||
tokenId: EASTER_EGG_TOKENID, | tokenId: EASTER_EGG_TOKENID, | ||||
}, | }, | ||||
}; | }; | ||||
// Modify walletWithXecAndTokens to have the required token for this feature | // Modify walletWithXecAndTokens to have the required token for this feature | ||||
let walletWithEasterEggToken = JSON.parse( | |||||
JSON.stringify(walletWithXecAndTokens), | const walletWithEasterEggToken = { | ||||
); | ...walletWithXecAndTokens, | ||||
walletWithEasterEggToken = { | |||||
...walletWithEasterEggToken, | |||||
state: { | state: { | ||||
...walletWithEasterEggToken.state, | ...walletWithXecAndTokens.state, | ||||
slpUtxos: [ | slpUtxos: [ | ||||
...walletWithEasterEggToken.state.slpUtxos, | ...walletWithXecAndTokens.state.slpUtxos, | ||||
requiredEasterEggUtxo, | requiredEasterEggUtxo, | ||||
], | ], | ||||
}, | }, | ||||
}; | }; | ||||
const mockedChronik = await initializeCashtabStateForTests( | const mockedChronik = await initializeCashtabStateForTests( | ||||
walletWithEasterEggToken, | walletWithEasterEggToken, | ||||
localforage, | localforage, | ||||
); | ); | ||||
// Make sure the app can get this token's genesis info by calling a mock | // Make sure the app can get this token's genesis info by calling a mock | ||||
mockedChronik.setMock('token', { | mockedChronik.setMock('token', { | ||||
input: EASTER_EGG_TOKENID, | input: EASTER_EGG_TOKENID, | ||||
output: easterEggTokenChronikTokenDetails, | output: easterEggTokenChronikTokenDetails, | ||||
}); | }); | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
// We see the easter egg | // We see the easter egg | ||||
expect(await screen.findByAltText('tabcash')).toBeInTheDocument(); | expect(await screen.findByAltText('tabcash')).toBeInTheDocument(); | ||||
}); | }); | ||||
it('If Cashtab starts with 1.5.* cashtabCache, it is wiped and migrated to 1.6.* cashtabCache', async () => { | it('If Cashtab starts with 1.5.* cashtabCache, it is wiped and migrated to 2.9.0 cashtabCache', async () => { | ||||
// Note: this is what will happen for all Cashtab users when this diff lands | // Note: this is what will happen for all Cashtab users when this diff lands | ||||
const mockedChronik = | const mockedChronik = | ||||
await initializeCashtabStateAtLegacyWalletKeysForTests( | await initializeCashtabStateAtLegacyWalletKeysForTests( | ||||
walletWithXecAndTokens, | walletWithXecAndTokens, | ||||
localforage, | localforage, | ||||
); | ); | ||||
// Mock cashtabCache at 1.5.* | // Mock cashtabCache at 1.5.* | ||||
await localforage.setItem('cashtabCache', legacyMockTokenInfoById); | await localforage.setItem('cashtabCache', legacyMockTokenInfoById); | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
const expectedCashtabCacheTokens = new Map(); | const expectedCashtabCacheTokens = new CashtabCache([ | ||||
[ | |||||
// Tokens from wallet utxos will be added to cache on app load | |||||
expectedCashtabCacheTokens.set( | |||||
'3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109', | '3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109', | ||||
{ | mockCacheWalletWithXecAndTokens, | ||||
decimals: 0, | ], | ||||
success: true, | ]); | ||||
hash: '', | |||||
url: 'https://cashtab.com/', | |||||
tokenName: 'BearNip', | |||||
tokenTicker: 'BEAR', | |||||
}, | |||||
); | |||||
// Result will be stored as a keyvalue array and must be converted to a map | |||||
// We do the reverse to get the expected storage value | |||||
const expectedStoredCashtabCache = cashtabCacheToJSON({ | |||||
tokens: expectedCashtabCacheTokens, | |||||
}); | |||||
// Confirm cashtabCache in localforage matches expected result | // Confirm cashtabCache in localforage matches expected result | ||||
await waitFor(async () => | await waitFor(async () => | ||||
expect(await localforage.getItem('cashtabCache')).toEqual( | expect( | ||||
expectedStoredCashtabCache, | storedCashtabCacheToMap( | ||||
await localforage.getItem('cashtabCache'), | |||||
), | ), | ||||
).toEqual(expectedCashtabCacheTokens), | |||||
); | ); | ||||
}); | }); | ||||
it('A new user can import a mnemonic', async () => { | it('A new user can import a mnemonic', async () => { | ||||
// Initialize for new user with wallet = false, so localstorage gets defaults | // Initialize for new user with wallet = false, so localstorage gets defaults | ||||
const mockedChronik = await initializeCashtabStateForTests( | const mockedChronik = await initializeCashtabStateForTests( | ||||
false, | false, | ||||
localforage, | localforage, | ||||
); | ); | ||||
▲ Show 20 Lines • Show All 53 Lines • ▼ Show 20 Lines | it('A new user can import a mnemonic', async () => { | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
// We are forwarded to the home screen after the wallet loads | // We are forwarded to the home screen after the wallet loads | ||||
expect(await screen.findByTestId('home-ctn')).toBeInTheDocument(); | expect(await screen.findByTestId('home-ctn')).toBeInTheDocument(); | ||||
// The imported wallet is in localforage | // The imported wallet is in localforage | ||||
const wallets = await localforage.getItem('wallets'); | const wallets = await localforage.getItem('wallets'); | ||||
const importedWallet = wallets[0]; | const importedWallet = cashtabWalletFromJSON(wallets[0]); | ||||
// The imported wallet matches our expected mock except for name, which is autoset on import | // The imported wallet matches our expected mock except for name, which is autoset on import | ||||
// The imported wallet is not imported with legacy paths (145 and 245) | // The imported wallet is not imported with legacy paths (145 and 245) | ||||
const expectedPathInfo = walletWithXecAndTokens.paths.find( | const expectedPathInfo = walletWithXecAndTokens.paths.get(1899); | ||||
pathInfo => pathInfo.path === 1899, | // We expect the wallet to be walletWithXecAndTokens, except new name and no legacy paths | ||||
); | const expectedWallet = { | ||||
expect(importedWallet).toEqual({ | |||||
...walletWithXecAndTokens, | ...walletWithXecAndTokens, | ||||
name: 'qqa9l', | name: 'qqa9l', | ||||
paths: [expectedPathInfo], | paths: new Map([[1899, expectedPathInfo]]), | ||||
}); | }; | ||||
expect(importedWallet).toEqual(expectedWallet); | |||||
// Apart from state, which is blank from createCashtabWallet, | // Apart from state, which is blank from createCashtabWallet, | ||||
// the imported wallet matches what we get from createCashtabWallet | // the imported wallet matches what we get from createCashtabWallet | ||||
const createdWallet = await createCashtabWallet(VALID_MNEMONIC); | const createdWallet = await createCashtabWallet(VALID_MNEMONIC); | ||||
expect(importedWallet).toEqual({ | expect(importedWallet).toEqual({ | ||||
...createdWallet, | ...createdWallet, | ||||
state: importedWallet.state, | state: importedWallet.state, | ||||
}); | }); | ||||
Show All 11 Lines | it('Migrating from wallet/savedWallet keys (version < 1.6.0): A user with an invalid Cashtab wallet as the active wallet is migrated on startup', async () => { | ||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | // Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | ||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
// Check wallet in localforage | // Check wallet in localforage | ||||
const wallets = await localforage.getItem('wallets'); | const wallets = await localforage.getItem('wallets'); | ||||
const migratedWallet = wallets[0]; | const migratedWallet = cashtabWalletFromJSON(wallets[0]); | ||||
// The wallet has been migrated | // The wallet has been migrated | ||||
expect(migratedWallet).toEqual(walletWithXecAndTokens); | expect(migratedWallet).toEqual(walletWithXecAndTokens); | ||||
}); | }); | ||||
it('Migrating from wallet/savedWallet keys (version < 1.6.0): A user with pre-2.1.0 valid wallets in savedWallets has them all migrated to new storage keys and new shape', async () => { | it('Migrating from wallet/savedWallet keys (version < 1.6.0): A user with pre-2.1.0 valid wallets in savedWallets has them all migrated to new storage keys and new shape', async () => { | ||||
const mockedChronik = | const mockedChronik = | ||||
await initializeCashtabStateAtLegacyWalletKeysForTests( | await initializeCashtabStateAtLegacyWalletKeysForTests( | ||||
// Any wallet stored in legacy key structures will have pre_2_1_0 format (or earlier) | // Any wallet stored in legacy key structures will have pre_2_1_0 format (or earlier) | ||||
Show All 13 Lines | it('Migrating from wallet/savedWallet keys (version < 1.6.0): A user with pre-2.1.0 valid wallets in savedWallets has them all migrated to new storage keys and new shape', async () => { | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | // Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | ||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
// Check wallets | // Check wallets | ||||
const walletsAfterLoad = await localforage.getItem('wallets'); | const walletsAfterLoad = cashtabWalletsFromJSON( | ||||
await localforage.getItem('wallets'), | |||||
); | |||||
const savedWallets = walletsAfterLoad.slice(1); | const savedWallets = walletsAfterLoad.slice(1); | ||||
// The savedWallets array stored at the savedWallets key is unchanged | // The savedWallets array stored at the savedWallets key is unchanged | ||||
expect(savedWallets).toEqual(validSavedWallets); | expect(savedWallets).toEqual(validSavedWallets); | ||||
}); | }); | ||||
it('Migrating (version >= 1.6.0 and < 2.1.0): A user with an invalid wallet stored at wallets key has that wallet migrated', async () => { | it('Migrating (version >= 1.6.0 and < 2.1.0): A user with an invalid wallet stored at wallets key has that wallet migrated', async () => { | ||||
// Create a savedWallets array with 4 valid wallets and 1 invalid wallet | // Create a savedWallets array with 4 valid wallets and 1 invalid wallet | ||||
Show All 14 Lines | it('Migrating (version >= 1.6.0 and < 2.1.0): A user with an invalid wallet stored at wallets key has that wallet migrated', async () => { | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | // Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | ||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
// Check wallets | // Check wallets | ||||
const walletsAfterLoad = await localforage.getItem('wallets'); | const walletsAfterLoad = cashtabWalletsFromJSON( | ||||
await localforage.getItem('wallets'), | |||||
); | |||||
const savedWallets = walletsAfterLoad.slice(1); | const savedWallets = walletsAfterLoad.slice(1); | ||||
// We expect savedWallets in localforage to have been migrated | // We expect savedWallets in localforage to have been migrated | ||||
await waitFor(async () => { | await waitFor(async () => { | ||||
expect(savedWallets).toEqual(validSavedWallets); | expect(savedWallets).toEqual(validSavedWallets); | ||||
}); | }); | ||||
}); | }); | ||||
it('Migrating (version >= 1.6.0 and < 2.1.0): A user with multiple invalid wallets in savedWallets has them migrated', async () => { | it('Migrating (version >= 1.6.0 and < 2.1.0): A user with multiple invalid wallets stored at wallets key has them migrated', async () => { | ||||
// Create a savedWallets array with 4 valid wallets and 1 invalid wallet | // Create a savedWallets array with 4 valid wallets and 1 invalid wallet | ||||
const mixedValidWallets = [ | const mixedValidWallets = [ | ||||
walletWithXecAndTokens, | walletWithXecAndTokens, | ||||
...validSavedWallets_pre_2_1_0.slice(0, 3), | ...validSavedWallets_pre_2_1_0.slice(0, 3), | ||||
...validSavedWallets.slice(3), | ...validSavedWallets.slice(3), | ||||
]; | ]; | ||||
// The wallets at indices 1, 2, and 3 are invalid | // The wallets at indices 1, 2, and 3 are invalid | ||||
Show All 9 Lines | it('Migrating (version >= 1.6.0 and < 2.1.0): A user with multiple invalid wallets stored at wallets key has them migrated', async () => { | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | // Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | ||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
// Check wallets | // Check wallets | ||||
const walletsAfterLoad = await localforage.getItem('wallets'); | const walletsAfterLoad = cashtabWalletsFromJSON( | ||||
await localforage.getItem('wallets'), | |||||
); | |||||
const savedWallets = walletsAfterLoad.slice(1); | const savedWallets = walletsAfterLoad.slice(1); | ||||
// We expect savedWallets in localforage to have been migrated | // We expect savedWallets in localforage to have been migrated | ||||
await waitFor(async () => { | await waitFor(async () => { | ||||
expect(savedWallets).toEqual(validSavedWallets); | expect(savedWallets).toEqual(validSavedWallets); | ||||
}); | }); | ||||
}); | }); | ||||
it('Cashtab version >= 1.6.0 and < 2.1.0: A user with an invalid Cashtab wallet as the active wallet is migrated on startup', async () => { | it('Cashtab version >= 1.6.0 and < 2.1.0: A user with an invalid Cashtab wallet as the active wallet is migrated on startup', async () => { | ||||
const mockedChronik = await initializeCashtabStateForTests( | const mockedChronik = await initializeCashtabStateForTests( | ||||
walletWithXecAndTokens_pre_2_1_0, | walletWithXecAndTokens_pre_2_1_0, | ||||
localforage, | localforage, | ||||
); | ); | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | // Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | ||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
// Check wallet in localforage | // Check wallet in localforage | ||||
const wallets = await localforage.getItem('wallets'); | const wallets = await localforage.getItem('wallets'); | ||||
const migratedWallet = wallets[0]; | const migratedWallet = cashtabWalletFromJSON(wallets[0]); | ||||
// The wallet has been migrated | // The wallet has been migrated | ||||
expect(migratedWallet).toEqual(walletWithXecAndTokens); | expect(migratedWallet).toEqual(walletWithXecAndTokens); | ||||
}); | }); | ||||
it('A user with all valid wallets in savedWallets does not have any savedWallets migrated', async () => { | it('A user with all valid wallets stored at wallets key does not have any wallets migrated', async () => { | ||||
const mockedChronik = await initializeCashtabStateForTests( | const mockedChronik = await initializeCashtabStateForTests( | ||||
[walletWithXecAndTokens, ...validSavedWallets], | [walletWithXecAndTokens, ...validSavedWallets], | ||||
localforage, | localforage, | ||||
); | ); | ||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | render(<CashtabTestWrapper chronik={mockedChronik} />); | ||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | // Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | ||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | ||||
'9,513.12 XEC', | '9,513.12 XEC', | ||||
); | ); | ||||
const walletsAfterLoad = cashtabWalletsFromJSON( | |||||
await localforage.getItem('wallets'), | |||||
); | |||||
// The savedWallets array stored at the savedWallets key is unchanged | // The savedWallets array stored at the savedWallets key is unchanged | ||||
expect(await localforage.getItem('wallets')).toEqual([ | expect(walletsAfterLoad).toEqual([ | ||||
walletWithXecAndTokens, | walletWithXecAndTokens, | ||||
...validSavedWallets, | ...validSavedWallets, | ||||
]); | ]); | ||||
}); | }); | ||||
it('Migrating (version < 2.9.0): A user with multiple invalid wallets stored at wallets key has them migrated', async () => { | |||||
// Create a savedWallets array with 4 valid wallets and 1 invalid wallet | |||||
const mixedValidWallets = [ | |||||
walletWithXecAndTokens, | |||||
...validSavedWallets_pre_2_9_0.slice(0, 3), | |||||
...validSavedWallets.slice(3), | |||||
]; | |||||
// The wallets at indices 1, 2, and 3 are invalid | |||||
expect(isValidCashtabWallet(mixedValidWallets[1])).toBe(false); | |||||
expect(isValidCashtabWallet(mixedValidWallets[2])).toBe(false); | |||||
expect(isValidCashtabWallet(mixedValidWallets[3])).toBe(false); | |||||
const mockedChronik = await initializeCashtabStateForTests( | |||||
mixedValidWallets, | |||||
localforage, | |||||
); | |||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | |||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | |||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | |||||
'9,513.12 XEC', | |||||
); | |||||
// Check wallets | |||||
const walletsAfterLoad = cashtabWalletsFromJSON( | |||||
await localforage.getItem('wallets'), | |||||
); | |||||
const savedWallets = walletsAfterLoad.slice(1); | |||||
// We expect savedWallets in localforage to have been migrated | |||||
await waitFor(async () => { | |||||
expect(savedWallets).toEqual(validSavedWallets); | |||||
}); | |||||
}); | |||||
it('Migrating (version < 2.9.0): A user with an invalid Cashtab wallet as the active wallet is migrated on startup', async () => { | |||||
const mockedChronik = await initializeCashtabStateForTests( | |||||
walletWithXecAndTokens_pre_2_9_0, | |||||
localforage, | |||||
); | |||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | |||||
// Wait balance to be rendered correctly so we know Cashtab has loaded the wallet | |||||
expect(await screen.findByTestId('balance-xec')).toHaveTextContent( | |||||
'9,513.12 XEC', | |||||
); | |||||
// Check wallet in localforage | |||||
const wallets = await localforage.getItem('wallets'); | |||||
const migratedWallet = cashtabWalletFromJSON(wallets[0]); | |||||
// The wallet has been migrated | |||||
expect(migratedWallet).toEqual(walletWithXecAndTokens); | |||||
}); | |||||
it('If Cashtab starts with < 2.9.0 cashtabCache, it is wiped and migrated to 2.9.0 cashtabCache', async () => { | |||||
// Note: this is what will happen for all Cashtab users when this diff lands | |||||
const mockedChronik = await initializeCashtabStateForTests( | |||||
walletWithXecAndTokens, | |||||
localforage, | |||||
); | |||||
// Mock cashtabCache at > 1.5.0 and < 2.9.0 | |||||
const pre_2_9_0_tokens_cache = new Map(); | |||||
// Tokens from wallet utxos will be added to cache on app load | |||||
pre_2_9_0_tokens_cache.set( | |||||
'3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109', | |||||
{ | |||||
decimals: 0, | |||||
success: true, | |||||
hash: '', | |||||
url: 'https://cashtab.com/', | |||||
tokenName: 'BearNip', | |||||
tokenTicker: 'BEAR', | |||||
}, | |||||
); | |||||
// Result will be stored as a keyvalue array and must be converted to a map | |||||
// We do the reverse to get the expected storage value | |||||
const expectedStoredCashtabCache = cashtabCacheToJSON({ | |||||
tokens: pre_2_9_0_tokens_cache, | |||||
}); | |||||
await localforage.setItem('cashtabCache', expectedStoredCashtabCache); | |||||
render(<CashtabTestWrapper chronik={mockedChronik} />); | |||||
// Confirm cashtabCache has been migrated to post-2.9.0 format | |||||
await waitFor(async () => | |||||
expect( | |||||
storedCashtabCacheToMap( | |||||
await localforage.getItem('cashtabCache'), | |||||
), | |||||
).toEqual( | |||||
new CashtabCache([ | |||||
[ | |||||
'3fee3384150b030490b7bee095a63900f66a45f2d8e3002ae2cf17ce3ef4d109', | |||||
{ | |||||
tokenType: { | |||||
protocol: 'SLP', | |||||
type: 'SLP_TOKEN_TYPE_FUNGIBLE', | |||||
number: 1, | |||||
}, | |||||
genesisInfo: { | |||||
tokenTicker: 'BEAR', | |||||
tokenName: 'BearNip', | |||||
url: 'https://cashtab.com/', | |||||
decimals: 0, | |||||
hash: '', | |||||
}, | |||||
timeFirstSeen: 0, | |||||
genesisSupply: '4444', | |||||
genesisOutputScripts: [ | |||||
'76a91495e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d88ac', | |||||
], | |||||
genesisMintBatons: 0, | |||||
block: { | |||||
height: 782665, | |||||
hash: '00000000000000001239831f90580c859ec174316e91961cf0e8cde57c0d3acb', | |||||
timestamp: 1678408305, | |||||
}, | |||||
}, | |||||
], | |||||
]), | |||||
), | |||||
); | |||||
}); | |||||
}); | }); |