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 @@ -207,8 +207,8 @@ id="borderedQRCode" address={ address === 'slpAddress' - ? wallet.Path245.slpAddress - : wallet.Path145.cashAddress + ? wallet.Path1899.slpAddress + : wallet.Path1899.cashAddress } /> @@ -233,7 +233,7 @@ {balances.totalBalance ? ( <> 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 @@ -10,6 +10,12 @@ cashAddress: 'bitcoincash:qrn4er57cvr5fulyl4hduef6czgu6u2yu522f4gv6f', }, + Path1899: { + slpAddress: + 'simpleledger:qryupy05jz7tlhtda2xth8vyvdqksyqh5cp5kf5vth', + cashAddress: + 'bitcoincash:qrn4er57cvr5fulyl4hduef6czgu6u2yu522f4gv6f', + }, }, balances: { totalBalance: 0.000546, @@ -26,6 +32,12 @@ cashAddress: 'bitcoincash:qrn4er57cvr5fulyl4hduef6czgu6u2yu522f4gv6f', }, + Path1899: { + slpAddress: + 'simpleledger:qryupy05jz7tlhtda2xth8vyvdqksyqh5cp5kf5vth', + cashAddress: + 'bitcoincash:qrn4er57cvr5fulyl4hduef6czgu6u2yu522f4gv6f', + }, }, tokens: [], balances: { @@ -43,6 +55,12 @@ cashAddress: 'bitcoincash:qrn4er57cvr5fulyl4hduef6czgu6u2yu522f4gv6f', }, + Path1899: { + slpAddress: + 'simpleledger:qryupy05jz7tlhtda2xth8vyvdqksyqh5cp5kf5vth', + cashAddress: + 'bitcoincash:qrn4er57cvr5fulyl4hduef6czgu6u2yu522f4gv6f', + }, }, balances: { totalBalances: 0.0000546, diff --git a/web/cashtab/src/hooks/__mocks__/sendBCH.js b/web/cashtab/src/hooks/__mocks__/sendBCH.js --- a/web/cashtab/src/hooks/__mocks__/sendBCH.js +++ b/web/cashtab/src/hooks/__mocks__/sendBCH.js @@ -20,6 +20,10 @@ cashAddress: 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05', }, + Path1899: { + cashAddress: + 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05', + }, }, addresses: ['bitcoincash:qr2npxqwznhp7gphatcqzexeclx0hhwdxg386ez36n'], values: ['0.00000546'], diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -301,12 +301,33 @@ // Return any token change back to the sender. if (slpSendObj.outputs > 1) { - transactionBuilder.addOutput( - BCH.SLP.Address.toLegacyAddress( - tokenUtxosBeingSpent[0].address, - ), - 546, - ); + // Try to send this to Path1899 to move all utxos off legacy addresses + if (wallet.Path1899.legacyAddress) { + /* + console.log( + `Path1899 legacy address found, sending SLP change to ${wallet.Path1899.slpAddress}`, + ); + */ + transactionBuilder.addOutput( + wallet.Path1899.legacyAddress, + 546, + ); + } else { + // If you can't, send it back from whence it came + /* + console.log( + `No Path1899 address found, sending SLP change to ${BCH.SLP.Address.toLegacyAddress( + tokenUtxosBeingSpent[0].address, + )}`, + ); + */ + transactionBuilder.addOutput( + BCH.SLP.Address.toLegacyAddress( + tokenUtxosBeingSpent[0].address, + ), + 546, + ); + } } // get byte count to calculate fee. paying 1 sat @@ -323,11 +344,26 @@ if (remainder < 1) { throw new Error('Selected UTXO does not have enough satoshis'); } + // Last output: send the BCH change back to the wallet. - transactionBuilder.addOutput( - BCH.Address.toLegacyAddress(largestBchUtxo.address), - remainder, - ); + // If Path1899, send it to Path1899 address + if (wallet.Path1899.legacyAddress) { + /* + console.log( + `Path1899 legacy address found, sending BCH change to ${wallet.Path1899.cashAddress}`, + ); + */ + transactionBuilder.addOutput( + wallet.Path1899.legacyAddress, + remainder, + ); + } else { + // Otherwise send it back from whence it came + transactionBuilder.addOutput( + BCH.Address.toLegacyAddress(largestBchUtxo.address), + remainder, + ); + } // Sign the transaction with the private key for the BCH UTXO paying the fees. let redeemScript; @@ -342,7 +378,7 @@ // Sign each token UTXO being consumed. for (let i = 0; i < tokenUtxosBeingSpent.length; i++) { const thisUtxo = tokenUtxosBeingSpent[i]; - const accounts = [wallet.Path245, wallet.Path145]; + const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; const utxoEcPair = BCH.ECPair.fromWIF( accounts .filter(acc => acc.cashAddress === thisUtxo.address) @@ -402,7 +438,7 @@ (previous, current) => new BigNumber(current).plus(previous), new BigNumber(0), ); - const REMAINDER_ADDR = wallet.Path145.cashAddress; + const REMAINDER_ADDR = wallet.Path1899.cashAddress; const inputUtxos = []; let transactionBuilder; 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 @@ -68,7 +68,7 @@ }; const normalizeSlpBalancesAndUtxos = (slpBalancesAndUtxos, wallet) => { - const Accounts = [wallet.Path245, wallet.Path145]; + const Accounts = [wallet.Path245, wallet.Path145, wallet.Path1899]; slpBalancesAndUtxos.nonSlpUtxos.forEach(utxo => { const derivatedAccount = Accounts.find( account => account.cashAddress === utxo.address, @@ -151,6 +151,7 @@ const cashAddresses = [ wallet.Path245.cashAddress, wallet.Path145.cashAddress, + wallet.Path1899.cashAddress, ]; const utxos = await getUtxos(BCH, cashAddresses); @@ -230,6 +231,12 @@ let existingWallet; try { existingWallet = await localforage.getItem('wallet'); + + // If the wallet does not have Path1899, add it + if (existingWallet && !existingWallet.Path1899) { + console.log(`Wallet does not have Path1899`); + existingWallet = await migrateLegacyWallet(existingWallet); + } // If not in localforage then existingWallet = false, check localstorage if (!existingWallet) { console.log(`no existing wallet, checking local storage`); @@ -268,6 +275,36 @@ return wallet; }; + const migrateLegacyWallet = async wallet => { + console.log(`migrateLegacyWallet`); + console.log(`legacyWallet`, wallet); + const NETWORK = process.env.REACT_APP_NETWORK; + const mnemonic = wallet.mnemonic; + const rootSeedBuffer = await BCH.Mnemonic.toSeed(mnemonic); + let masterHDNode; + + if (NETWORK === `mainnet`) + masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer); + else masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet'); + const Path1899 = await deriveAccount({ + masterHDNode, + path: "m/44'/1899'/0'/0/0", + }); + + wallet.Path1899 = Path1899; + + try { + await localforage.setItem('wallet', wallet); + } catch (err) { + console.log( + `Error setting wallet to wallet indexedDb in migrateLegacyWallet()`, + ); + console.log(err); + } + + return wallet; + }; + const getWalletDetails = async wallet => { if (!wallet) { return false; @@ -290,6 +327,10 @@ masterHDNode, path: "m/44'/145'/0'/0/0", }); + const Path1899 = await deriveAccount({ + masterHDNode, + path: "m/44'/1899'/0'/0/0", + }); let name = Path145.cashAddress.slice(12, 17); // Only set the name if it does not currently exist @@ -302,6 +343,7 @@ name, Path245, Path145, + Path1899, }; }; @@ -356,14 +398,52 @@ ); return false; } + /* + When a legacy user runs cashtabapp.com/, their active wallet will be migrated to Path1899 by + the getWallet function + + Wallets in savedWallets are migrated when they are activated, in this function + + Two cases to handle + + 1 - currentlyActiveWallet has Path1899, but its stored keyvalue pair in savedWallets does not + > Update savedWallets so that Path1899 is added to currentlyActiveWallet + + 2 - walletToActivate does not have Path1899 + > Update walletToActivate with Path1899 before activation + */ // Check savedWallets for currentlyActiveWallet let walletInSavedWallets = false; + let walletUnmigrated = false; for (let i = 0; i < savedWallets.length; i += 1) { if (savedWallets[i].name === currentlyActiveWallet.name) { walletInSavedWallets = true; + // Check savedWallets for unmigrated currentlyActiveWallet + if (!savedWallets[i].Path1899) { + // Case 1, described above + console.log( + `Case 1: Wallet migration in saved wallets still pending, adding Path1899`, + ); + savedWallets[i].Path1899 = currentlyActiveWallet.Path1899; + walletUnmigrated = true; + } + } + } + + // Case 1 + if (walletUnmigrated) { + // resave savedWallets + try { + // Set walletName as the active wallet + await localforage.setItem('savedWallets', savedWallets); + } catch (err) { + console.log( + `Error in localforage.setItem("savedWallets") in activateWallet() for unmigrated wallet`, + ); } } + if (!walletInSavedWallets) { console.log(`Wallet is not in saved Wallets, adding`); savedWallets.push(currentlyActiveWallet); @@ -377,16 +457,28 @@ ); } } + // If wallet does not have Path1899, add it - // Now that we have verified the last wallet was saved, we can activate the new wallet - try { - await localforage.setItem('wallet', walletToActivate); - } catch (err) { + if (!walletToActivate.Path1899) { + // Case 2, described above + console.log(`Case 2: Wallet to activate does not have Path1899`); console.log( - `Error in localforage.setItem("wallet", walletToActivate) in activateWallet()`, + `Wallet to activate from SavedWallets does not have Path1899`, ); - return false; + walletToActivate = await migrateLegacyWallet(walletToActivate); + } else { + // Otherwise activate it as normal + // Now that we have verified the last wallet was saved, we can activate the new wallet + try { + await localforage.setItem('wallet', walletToActivate); + } catch (err) { + console.log( + `Error in localforage.setItem("wallet", walletToActivate) in activateWallet()`, + ); + return false; + } } + return walletToActivate; };