diff --git a/web/cashtab/src/hooks/__mocks__/mockLegacyWallets.js b/web/cashtab/src/hooks/__mocks__/mockLegacyWallets.js --- a/web/cashtab/src/hooks/__mocks__/mockLegacyWallets.js +++ b/web/cashtab/src/hooks/__mocks__/mockLegacyWallets.js @@ -38,6 +38,8 @@ fundingAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', }, Path145: { cashAddress: @@ -48,6 +50,8 @@ fundingAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', }, Path1899: { cashAddress: @@ -58,6 +62,8 @@ fundingAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', }, }, @@ -94,6 +100,8 @@ fundingWif: 'cN3NDtiabeeX1Wzg2CPgLgrb7fsDG8PgWqTnmwAhYWmY6osSta7Q', legacyAddress: 'muCLmiGfH961UuWrTTFNU3KxH9pVQJJx6Z', slpAddress: 'slptest:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5234lu2sx', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', }, Path145: { cashAddress: 'bchtest:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vtdvpkdz7', @@ -102,6 +110,8 @@ fundingWif: 'cTKuvZ644Sf7xra5z3dPshEECJNaDyBCq7HyBsysQPh1ySSpxtQ1', legacyAddress: 'mjWxk74mLM7TieAsSJArLF7nXqC6oc2mof', slpAddress: 'slptest:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vset6v6sr', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', }, Path1899: { cashAddress: 'bchtest:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7c9ex06hrx', @@ -110,6 +120,8 @@ fundingWif: 'cNRFB6MmkNhyhAj1TpGhXdbHgzWg4BsdHbAkKjiz4vt4vwgpC44F', legacyAddress: 'mxX888y8yaPpTYh3WhrB9GEkT3cgumYwPw', slpAddress: 'slptest:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7c7dp5qq3m', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', }, }, }; diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -9,6 +9,7 @@ fromSmallestDenomination, loadStoredWallet, isValidStoredWallet, + isLegacyMigrationRequired, } from '@utils/cashMethods'; import { isValidCashtabSettings } from '@utils/validation'; import localforage from 'localforage'; @@ -108,10 +109,12 @@ const deriveAccount = async (BCH, { masterHDNode, path }) => { const node = BCH.HDNode.derivePath(masterHDNode, path); + const publicKey = BCH.HDNode.toPublicKey(node).toString('hex'); const cashAddress = BCH.HDNode.toCashAddress(node); const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress); return { + publicKey, cashAddress, slpAddress, fundingWif: BCH.HDNode.toWIF(node), @@ -325,10 +328,19 @@ // 3 - null if error // If the wallet does not have Path1899, add it - if (existingWallet && !existingWallet.Path1899) { - console.log(`Wallet does not have Path1899`); - existingWallet = await migrateLegacyWallet(BCH, existingWallet); + // or each Path1899, Path145, Path245 does not have a public key, add them + if (existingWallet) { + if (isLegacyMigrationRequired(existingWallet)) { + console.log( + `Wallet does not have Path1899 or does not have public key`, + ); + existingWallet = await migrateLegacyWallet( + BCH, + existingWallet, + ); + } } + // If not in localforage then existingWallet = false, check localstorage if (!existingWallet) { console.log(`no existing wallet, checking local storage`); @@ -378,11 +390,22 @@ } else { masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet'); } + + const Path245 = await deriveAccount(BCH, { + masterHDNode, + path: "m/44'/245'/0'/0/0", + }); + const Path145 = await deriveAccount(BCH, { + masterHDNode, + path: "m/44'/145'/0'/0/0", + }); const Path1899 = await deriveAccount(BCH, { masterHDNode, path: "m/44'/1899'/0'/0/0", }); + wallet.Path245 = Path245; + wallet.Path145 = Path145; wallet.Path1899 = Path1899; try { @@ -505,7 +528,7 @@ } /* When a legacy user runs cashtab.com/, their active wallet will be migrated to Path1899 by - the getWallet function + the getWallet function. getWallet function also makes sure that each Path has a public key Wallets in savedWallets are migrated when they are activated, in this function @@ -516,6 +539,9 @@ 2 - walletToActivate does not have Path1899 > Update walletToActivate with Path1899 before activation + + NOTE: since publicKey property is added later, + wallet without public key in Path1899 is also considered legacy and required migration. */ // Need to handle a similar situation with state @@ -528,9 +554,11 @@ if (savedWallets[i].name === currentlyActiveWallet.name) { walletInSavedWallets = true; // Check savedWallets for unmigrated currentlyActiveWallet - if (!savedWallets[i].Path1899) { + if (isLegacyMigrationRequired(savedWallets[i])) { // Case 1, described above savedWallets[i].Path1899 = currentlyActiveWallet.Path1899; + savedWallets[i].Path145 = currentlyActiveWallet.Path145; + savedWallets[i].Path245 = currentlyActiveWallet.Path245; } /* @@ -566,12 +594,15 @@ } } // If wallet does not have Path1899, add it - - if (!walletToActivate.Path1899) { + // or each of the Path1899, Path145, Path245 does not have a public key, add them + // by calling migrateLagacyWallet() + if (isLegacyMigrationRequired(walletToActivate)) { // Case 2, described above - console.log(`Case 2: Wallet to activate does not have Path1899`); console.log( - `Wallet to activate from SavedWallets does not have Path1899`, + `Case 2: Wallet to activate does not have Path1899 or does not have public key in each Path`, + ); + console.log( + `Wallet to activate from SavedWallets does not have Path1899 or does not have public key in each Path`, ); console.log(`walletToActivate`, walletToActivate); walletToActivate = await migrateLegacyWallet(BCH, walletToActivate); diff --git a/web/cashtab/src/utils/__mocks__/mockLegacyWallets.js b/web/cashtab/src/utils/__mocks__/mockLegacyWallets.js new file mode 100644 --- /dev/null +++ b/web/cashtab/src/utils/__mocks__/mockLegacyWallets.js @@ -0,0 +1,164 @@ +export const missingPath1899Wallet = { + Path145: { + cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path245: { + cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Missing Path1899', + state: {}, +}; + +export const missingPublicKeyInPath1899Wallet = { + Path145: { + cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Missing Public Key in Path1899', + state: {}, +}; + +export const missingPublicKeyInPath145Wallet = { + Path145: { + cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Missing Public Key in Path145', + state: {}, +}; + +export const missingPublicKeyInPath245Wallet = { + Path145: { + cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Missing Public Key in Path245', + state: {}, +}; + +export const notLegacyWallet = { + Path145: { + cashAddress: 'bitcoincash:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9v0lgx569z', + fundingAddress: + 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + fundingWif: 'L2xvTe6CdNxroR6pbdpGWNjAa55AZX5Wm59W5TXMuH31ihNJdDjt', + legacyAddress: '1511T3ynXKgCwXhFijCUWKuTfqbPxFV1AF', + publicKey: + '02fe5308d77bcce825068a9e46adc6f032dbbe39167a7b6d05ac563ac71d8b186e', + slpAddress: 'simpleledger:qq47pcxfn8n7w7jy86njd7pvgsv39l9f9vryrap6mu', + }, + Path1899: { + cashAddress: 'bitcoincash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cptzgcqy6', + fundingAddress: + 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + fundingWif: 'Kx4FiBMvKK1iXjFk5QTaAK6E4mDGPjmwDZ2HDKGUZpE4gCXMaPe9', + legacyAddress: '1J1Aq5tAAYxZgSDRo8soKM2Rb41z3xrYpm', + publicKey: + '02a06bb380cf180d703f6f80796a13555aefff817d1f6f842f1e5c555b15f0fa70', + slpAddress: 'simpleledger:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7cdsfndq6y', + }, + Path245: { + cashAddress: 'bitcoincash:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy54hkry298', + fundingAddress: + 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + fundingWif: 'KwgNkyijAaxFr5XQdnaYyNMXVSZobgHzSoKKfWiC3Q7Xr4n7iYMG', + legacyAddress: '1EgPUfBgU7ekho3EjtGze87dRADnUE8ojP', + publicKey: + '03c4a69fd90c8b196683216cffd2943a7b13b0db0812e44a4ff156ac7e03fc4ed7', + slpAddress: 'simpleledger:qztqe8k4v8ckn8cvfxt5659nhd7dcyvxy5evac32me', + }, + mnemonic: + 'apart vacuum color cream drama kind foil history hurt alone ask census', + name: 'Current Wallet, Not Legacy', + state: {}, +}; diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js --- a/web/cashtab/src/utils/__tests__/cashMethods.test.js +++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js @@ -10,6 +10,7 @@ convertEtokenToSimpleledger, checkNullUtxosForTokenStatus, confirmNonEtokenUtxos, + isLegacyMigrationRequired, } from '@utils/cashMethods'; import { unbatchedArray, @@ -37,6 +38,14 @@ mockHydratedUtxosWithNullValuesSetToFalse, } from '../__mocks__/nullUtxoMocks'; +import { + missingPath1899Wallet, + missingPublicKeyInPath1899Wallet, + missingPublicKeyInPath145Wallet, + missingPublicKeyInPath245Wallet, + notLegacyWallet, +} from '../__mocks__/mockLegacyWallets'; + describe('Correctly executes cash utility functions', () => { it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => { expect(fromSmallestDenomination(1, 2)).toBe(0.01); @@ -168,4 +177,25 @@ ), ).toStrictEqual(mockHydratedUtxosWithNullValuesSetToFalse); }); + it(`Recognizes a wallet with missing Path1889 is a Legacy Wallet and requires migration`, () => { + expect(isLegacyMigrationRequired(missingPath1899Wallet)).toBe(true); + }); + it(`Recognizes a wallet with missing PublicKey in Path1889 is a Legacy Wallet and requires migration`, () => { + expect( + isLegacyMigrationRequired(missingPublicKeyInPath1899Wallet), + ).toBe(true); + }); + it(`Recognizes a wallet with missing PublicKey in Path145 is a Legacy Wallet and requires migration`, () => { + expect(isLegacyMigrationRequired(missingPublicKeyInPath145Wallet)).toBe( + true, + ); + }); + it(`Recognizes a wallet with missing PublicKey in Path245 is a Legacy Wallet and requires migration`, () => { + expect(isLegacyMigrationRequired(missingPublicKeyInPath245Wallet)).toBe( + true, + ); + }); + it(`Recognizes a latest, current wallet that does not require migration`, () => { + expect(isLegacyMigrationRequired(notLegacyWallet)).toBe(false); + }); }); diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js --- a/web/cashtab/src/utils/cashMethods.js +++ b/web/cashtab/src/utils/cashMethods.js @@ -275,3 +275,19 @@ } return nonEtokenUtxos; }; + +export const isLegacyMigrationRequired = wallet => { + // If the wallet does not have Path1899, + // Or each Path1899, Path145, Path245 does not have a public key + // Then it requires migration + if ( + !wallet.Path1899 || + !wallet.Path1899.publicKey || + !wallet.Path145.publicKey || + !wallet.Path245.publicKey + ) { + return true; + } + + return false; +};