From 1f27e7269dae6e756e7d3d5a31dd16fc935b6866 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Mon, 4 Mar 2024 14:41:55 -0300 Subject: [PATCH 01/10] chore: updates dependabot branch (#536) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c20e993f..3f1673d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,6 @@ updates: directory: / schedule: interval: "weekly" - target-branch: "dev" + target-branch: "master" labels: - "dependencies" From 54ef74e52eae6ef0ae916fdc434cd97cf5cfff9c Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Tue, 5 Mar 2024 12:15:08 -0300 Subject: [PATCH 02/10] Fix: Server change issues (#537) * fix: invalid token crash on network change * fix: crash on reload wallet * fix: crash when resetting wallet on testnet * fix: updates local store besides the lib config --- src/components/RequestError.js | 12 +++-- src/sagas/wallet.js | 5 +- src/screens/Server.js | 7 ++- src/storage.js | 2 +- src/utils/wallet.js | 93 ---------------------------------- 5 files changed, 18 insertions(+), 101 deletions(-) diff --git a/src/components/RequestError.js b/src/components/RequestError.js index bd775014..fcb97052 100644 --- a/src/components/RequestError.js +++ b/src/components/RequestError.js @@ -10,9 +10,8 @@ import { t } from 'ttag'; import $ from 'jquery'; import createRequestInstance from '../api/axiosInstance'; import SpanFmt from './SpanFmt'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import hathorLib from '@hathor/wallet-lib'; -import wallet from '../utils/wallet'; import { useHistory } from 'react-router-dom'; /** @@ -28,8 +27,11 @@ const MODAL_DOM_ID = '#requestErrorModal'; */ function RequestErrorModal() { const history = useHistory(); - const lastFailedRequest = useSelector(state => state.lastFailedRequest); - const requestErrorStatusCode = useSelector(state => state.requestErrorStatusCode); + const dispatch = useDispatch(); + const { lastFailedRequest, requestErrorStatusCode } = useSelector(state => ({ + lastFailedRequest: state.lastFailedRequest, + requestErrorStatusCode: state.requestErrorStatusCode, + })); const advancedDataRef = useRef(); const showAdvancedLinkRef = useRef(); @@ -75,7 +77,7 @@ function RequestErrorModal() { // it's paginated with async/await and does not use promise resolve/reject like the others // We already have an issue on the lib to track this (https://github.com/HathorNetwork/hathor-wallet-lib/issues/59) // So we handle here this retry manually with a reload (like the one when the connection is lost) - wallet.reloadData(); + dispatch({ type: 'WALLET_RELOADING' }); } else { const config = lastFailedRequest; const axios = createRequestInstance(config.resolve); diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index b55cad06..cf2d80c0 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -194,7 +194,10 @@ export function* startWallet(action) { connection = wallet.conn; } else { // Set the server and network as saved on localStorage - config.setServerUrl(LOCAL_STORE.getServer()); + // If the localStorage is empty, fetch data directly from the lib config + const serverUrl = LOCAL_STORE.getServer() || config.getServerUrl(); + config.setServerUrl(serverUrl); + LOCAL_STORE.setServers(serverUrl); connection = new Connection({ network: network.name, diff --git a/src/screens/Server.js b/src/screens/Server.js index 9d50c8f9..d3b7de9e 100644 --- a/src/screens/Server.js +++ b/src/screens/Server.js @@ -22,7 +22,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; import LOCAL_STORE from '../storage'; import { useHistory } from 'react-router-dom'; -import { isVersionAllowedUpdate } from "../actions"; +import { isVersionAllowedUpdate, selectToken } from "../actions"; /** @@ -257,6 +257,11 @@ function Server() { // Forces the re-validation of the allowed version after server change dispatch(isVersionAllowedUpdate({ allowed: undefined })); + // Forces the selected token back to the default, as custom tokens will not be available in different networks + if (networkChanged) { + dispatch(selectToken(hathorLib.constants.HATHOR_TOKEN_CONFIG.uid)); + } + // We don't have PIN on hardware wallet const pin = isHardwareWallet ? null : pinRef.current.value; try { diff --git a/src/storage.js b/src/storage.js index e11cfcc5..00fa5e45 100644 --- a/src/storage.js +++ b/src/storage.js @@ -464,7 +464,7 @@ export class LocalStorageStore { /** * Persist server URLs on the localStorage. * @param {string} serverURL Fullnode api url - * @param {string} wsServerURL websocket server url for wallet-service + * @param {string} [wsServerURL] websocket server url for wallet-service */ setServers(serverURL, wsServerURL) { this.setItem(SERVER_KEY, serverURL); diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 6709fc0f..bd8d54df 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -12,10 +12,7 @@ import { } from '../constants'; import store from '../store/index'; import { - loadingAddresses, startWalletRequested, - historyUpdate, - reloadData, cleanData, updateTokenHistory, tokenMetadataUpdated, @@ -27,7 +24,6 @@ import { errors as hathorErrors, walletUtils, metadataApi, - storageUtils, Network, } from '@hathor/wallet-lib'; import { chunk, get } from 'lodash'; @@ -164,35 +160,6 @@ const wallet = { return newHistoryObjects; }, - /** - * Method that fetches the balance of a token - * and pre process for the expected format - * - * @param {HathorWallet} wallet wallet instance - * @param {String} uid Token uid to fetch balance - */ - async fetchTokenBalance(wallet, uid) { - const balance = await wallet.getBalance(uid); - const tokenBalance = balance[0].balance; - const authorities = balance[0].tokenAuthorities; - - let mint = false; - let melt = false; - - if (authorities) { - const { unlocked } = authorities; - mint = unlocked.mint; - melt = unlocked.melt; - } - - return { - available: tokenBalance.unlocked, - locked: tokenBalance.locked, - mint, - melt, - }; - }, - /** * The wallet needs each token metadata to show information correctly * So we fetch the tokens metadata and store on redux @@ -382,36 +349,6 @@ const wallet = { } }, - /** - * After load address history we should update redux data - * - * @param {HathorWallet} wallet The wallet instance - * - * @memberof Wallet - * @inner - */ - async afterLoadAddressHistory(wallet) { - // runs after load address history - store.dispatch(loadingAddresses(false)); - // XXX: We used to get the entire history of transactions, but it was not being used. - - const allTokens = await wallet.getTokens(); - const transactionsFound = await wallet.storage.store.historyCount(); - const addressesFound = await wallet.storage.store.addressCount(); - - const address = await wallet.storage.getCurrentAddress(); - const addressInfo = await wallet.storage.getAddressInfo(address); - const lastSharedIndex = addressInfo.bip32AddressIndex; - - store.dispatch(historyUpdate({ - allTokens, - lastSharedIndex, - lastSharedAddress: address, - addressesFound, - transactionsFound, - })); - }, - /** * Add passphrase to the wallet * @@ -462,36 +399,6 @@ const wallet = { store.dispatch(cleanData()); }, - /* - * Reload data in the localStorage - * - * @param {HathorWallet} wallet The wallet instance - * @param {Object} [options={}] - * @param {boolean} [options.endConnection=false] If should end connection with websocket - * - * @memberof Wallet - * @inner - */ - async reloadData(wallet, {endConnection = false} = {}) { - store.dispatch(loadingAddresses(true)); - - const tokens = new Set(); - for await (const token of wallet.storage.getRegisteredTokens()) { - tokens.add(token.uid); - } - - // Cleaning redux and leaving only tokens data - store.dispatch(reloadData({ - tokensHistory: {}, - tokensBalance: {}, - tokens, - })); - - // Load history from new server - await storageUtils.reloadStorage(wallet.storage, wallet.conn); - await this.afterLoadAddressHistory(wallet); - }, - /** * Verifies if user allowed to send errors to sentry * From 2525524892b1a29595ce8cf4582cb0a0dc6dde13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 7 Mar 2024 13:42:22 -0300 Subject: [PATCH 03/10] feat: start wallet with wallet-service (#533) * feat: wallet-service start wallet * chore: clean comment --- src/sagas/featureToggle.js | 1 + src/sagas/wallet.js | 7 +++++-- src/utils/helpers.js | 19 +++++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/sagas/featureToggle.js b/src/sagas/featureToggle.js index 6bb2b8b1..908d71b8 100644 --- a/src/sagas/featureToggle.js +++ b/src/sagas/featureToggle.js @@ -196,6 +196,7 @@ export function* setupUnleashListeners(unleashClient) { } function mapFeatureToggles(toggles) { + console.log('feature toggles', toggles); return toggles.reduce((acc, toggle) => { acc[toggle.name] = get( toggle, diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index cf2d80c0..22418592 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -149,6 +149,7 @@ export function* startWallet(action) { let wallet, connection; if (useWalletService) { + let authxpriv = null; // Set urls for wallet service. If we have it on storage, use it, otherwise use defaults try { // getWsServer can return null if not previously set @@ -172,13 +173,14 @@ export function* startWallet(action) { if (!(words || xpub)) { const accessData = yield storage.getAccessData(); - xpriv = cryptoUtils.decryptData(accessData.mainKey, pin); + xpriv = cryptoUtils.decryptData(accessData.acctPathKey, pin); + authxpriv = cryptoUtils.decryptData(accessData.authKey, pin); } const walletConfig = { seed: words, - xpub, xpriv, + authxpriv, requestPassword: async () => new Promise((resolve) => { /** * Lock screen will call `resolve` with the pin screen after validation @@ -188,6 +190,7 @@ export function* startWallet(action) { }), passphrase, storage, + network, }; wallet = new HathorWalletServiceWallet(walletConfig); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 16f0660c..0318d80f 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -269,10 +269,21 @@ const helpers = { * @returns {Promise} */ async mapTxHistoryToRedux(wallet, tx, tokenUid) { - // tx comes from getTxHistory and does not have token_data - // We need the actual history tx to access if it is an authority tx - const histTx = await wallet.getTx(tx.txId); - const isAllAuthority = this.isAllAuthority(histTx); + + let isAllAuthority = false; + try { + // tx comes from getTxHistory and does not have token_data + // We need the actual history tx to access if it is an authority tx + const histTx = await wallet.getTx(tx.txId); + const isAllAuthority = this.isAllAuthority(histTx); + } catch (err) { + // wallet-service facade does not implement getTx yet + // This will create a way to safely ignore the isAllAuthority if we cannot get the tx + if (err.message.toLowerCase() !== 'not implemented.') { + console.error(err); + throw err; + } + } return { tx_id: tx.txId, From b07a82d86d7801034117fa18c9c6a51616411470 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Wed, 20 Mar 2024 14:45:53 -0300 Subject: [PATCH 04/10] fix: Network name inconsistency (#547) * fix: failsafing also the network name * refactor: setting network name --- src/sagas/wallet.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index 22418592..98610445 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -124,7 +124,6 @@ export function* startWallet(action) { } } - const network = config.getNetwork(); const storage = LOCAL_STORE.getStorage(); // We are offline, the connection object is yet to be created @@ -147,6 +146,15 @@ export function* startWallet(action) { // then we don't know if we've cleaned up the wallet data in the storage yield storage.cleanStorage(true, true); + // Set the server and network as saved on localStorage + // If the localStorage is empty, fetch data directly from the lib config + const networkName = LOCAL_STORE.getNetwork() || config.getNetwork().name; + const serverUrl = LOCAL_STORE.getServer() || config.getServerUrl(); + config.setServerUrl(serverUrl); + config.setNetwork(networkName); + LOCAL_STORE.setServers(serverUrl); + LOCAL_STORE.setNetwork(networkName); + let wallet, connection; if (useWalletService) { let authxpriv = null; @@ -196,15 +204,9 @@ export function* startWallet(action) { wallet = new HathorWalletServiceWallet(walletConfig); connection = wallet.conn; } else { - // Set the server and network as saved on localStorage - // If the localStorage is empty, fetch data directly from the lib config - const serverUrl = LOCAL_STORE.getServer() || config.getServerUrl(); - config.setServerUrl(serverUrl); - LOCAL_STORE.setServers(serverUrl); - connection = new Connection({ - network: network.name, - servers: [config.getServerUrl()], + network: networkName, + servers: [serverUrl], }); const beforeReloadCallback = () => { @@ -250,16 +252,16 @@ export function* startWallet(action) { console.log('[+] Start wallet.', serverInfo); let version; - let networkName = network.name; + let serverNetworkName = networkName; if (serverInfo) { version = serverInfo.version; - networkName = serverInfo.network && serverInfo.network.split('-')[0]; + serverNetworkName = serverInfo.network && serverInfo.network.split('-')[0]; } yield put(setServerInfo({ version, - network: networkName, + network: serverNetworkName, })); } catch(e) { if (useWalletService) { @@ -285,7 +287,7 @@ export function* startWallet(action) { if (enableAtomicSwap) { // Set urls for the Atomic Swap Service. If we have it on storage, use it, otherwise use defaults - initializeSwapServiceBaseUrlForWallet(network.name) + initializeSwapServiceBaseUrlForWallet(networkName) // Initialize listened proposals list const listenedProposals = walletUtils.getListenedProposals(); yield put(proposalListUpdated(listenedProposals)); From 0a8b2554ef028da37ce877960d4d81df3df605ea Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Thu, 21 Mar 2024 13:43:01 -0300 Subject: [PATCH 05/10] fix: Ledger modal not updating on SendTokens (#546) * fix: Ledger modal not updating on SendTokens * fix: restores mandatory `cb` param * fix: event handlers out of sync * refactor: external variable moved to useRef * feat: adds modalState param --- src/screens/SendTokens.js | 99 ++++++++++++++++++++++++--------------- src/screens/Wallet.js | 1 + 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/screens/SendTokens.js b/src/screens/SendTokens.js index 80be7e9c..5557a32a 100644 --- a/src/screens/SendTokens.js +++ b/src/screens/SendTokens.js @@ -22,6 +22,12 @@ import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; import LOCAL_STORE from '../storage'; import { useHistory } from 'react-router-dom'; +/** @typedef {0|1} LEDGER_MODAL_STATE */ +const LEDGER_MODAL_STATE = { + WAITING_APPROVAL: 0, + LEDGER_APPROVED: 1, +} + /** * Screen used to send tokens to another wallet. * Can send more than one token in the same transaction. @@ -59,13 +65,15 @@ function SendTokens() { const [errorMessage, setErrorMessage] = useState(''); /** txTokens {Array} Array of tokens configs already added by the user (start with only hathor) */ const [txTokens, setTxTokens] = useState([...getSelectedToken()]); - /** ledgerStep {number} When sending tx with ledger we have a step that needs user physical input, - * then we move to next step */ - const [ledgerStep, setLedgerStep] = useState(0); // Create refs const formSendTokensRef = useRef(); const references = useRef([React.createRef()]); + /** + * Instance of SendTransaction containing tx data specifically for Ledger signing + * @type MutableRefObject + */ + const sendTransactionRef = useRef(null); // Convert componentDidMount and componentWillUnmount useEffect(() => { @@ -85,12 +93,6 @@ function SendTokens() { }; }, []); - // Partial transaction data while retrieving inputs and building the SendTransaction object - let newTxData = null; - - // Send transaction object used when sending tx with ledger - let sendTransaction = null; - /** * Handle the response of a send tx call to Ledger. * @@ -99,7 +101,7 @@ function SendTokens() { */ const handleTxSent = (_event, arg) => { if (arg.success) { - getSignatures(); + getSignatures(sendTransactionRef.current); } else { handleSendError(new LedgerError(arg.error.message)); } @@ -113,7 +115,7 @@ function SendTokens() { */ const handleSignatures = (_event, arg) => { if (arg.success) { - onLedgerSuccess(arg.data); + onLedgerSuccess(arg.data, sendTransactionRef.current); } else { handleSendError(new LedgerError(arg.error.message)); } @@ -137,7 +139,7 @@ function SendTokens() { return; } // some token was invalid, it will be on arg, which ones - const tokenList = txTokens.filter(t => arg.data.includes(t.uid)) // TODO: Double check if state is updated + const tokenList = txTokens.filter(t => arg.data.includes(t.uid)); globalModalContext.showModal(MODAL_TYPES.ALERT, { id: 'ledgerAlertModal', title: t`Invalid custom tokens`, @@ -154,7 +156,7 @@ function SendTokens() { * * @return {boolean} */ - const validateData = () => { + const validateFormData = () => { const isValid = formSendTokensRef.current.checkValidity(); if (isValid === false) { formSendTokensRef.current.classList.add('was-validated'); @@ -170,7 +172,7 @@ function SendTokens() { * * @return {Object} Object holding all inputs and outputs {'inputs': [...], 'outputs': [...]} */ - const getData = () => { + const getFormData = () => { let data = {'inputs': [], 'outputs': [], 'tokens': []}; for (const ref of references.current) { const instance = ref.current; @@ -184,8 +186,10 @@ function SendTokens() { /** * Add signature to each input and execute send transaction + * @param {String[]} signatures Array of serialized signatures to be injected on the tx + * @param {SendTransaction} sendTransaction Object containing the unsigned tx data */ - const onLedgerSuccess = async (signatures) => { + const onLedgerSuccess = async (signatures, sendTransaction) => { try { // Prepare data and submit job to tx mining API const arr = []; @@ -193,12 +197,11 @@ function SendTokens() { arr.push(Buffer.from(signatures[i])); } await sendTransaction.prepareTxFrom(arr); - setLedgerStep(1); globalModalContext.showModal(MODAL_TYPES.ALERT, { id: 'ledgerAlertModal', title: t`Validate outputs on Ledger`, - body: renderAlertBody(), + body: renderAlertBody(LEDGER_MODAL_STATE.LEDGER_APPROVED, sendTransaction), showFooter: false, }); } catch(e) { @@ -208,10 +211,12 @@ function SendTokens() { /** * Execute ledger get signatures + * @param {SendTransaction} sendTransaction instance to build the tx data from */ - const getSignatures = () => { + const getSignatures = (sendTransaction) => { + const txData = sendTransaction.fullTxData; ledger.getSignatures( - Object.assign({}, newTxData), + Object.assign({}, txData), wallet, ); } @@ -237,7 +242,6 @@ function SendTokens() { const onSendError = (message) => { globalModalContext.hideModal(); setErrorMessage(message); - setLedgerStep(0); } /** @@ -288,25 +292,25 @@ function SendTokens() { * It opens the ledger modal to wait for user action on the device */ const executeSendLedger = async () => { + let txData = getFormData(); // Wallet Service currently does not support Ledger, so we default to the regular SendTransaction - sendTransaction = new hathorLib.SendTransaction({ - outputs: newTxData.outputs, - inputs: newTxData.inputs, + const sendTransactionObj = new hathorLib.SendTransaction({ + outputs: txData.outputs, + inputs: txData.inputs, storage: wallet.storage, }); try { // Errors may happen in this step ( ex.: insufficient amount of tokens ) - newTxData = await sendTransaction.prepareTxData(); + txData = await sendTransactionObj.prepareTxData(); } catch (e) { setErrorMessage(e.message); - setLedgerStep(0); return; } const changeInfo = []; - for (const [outputIndex, output] of newTxData.outputs.entries()) { + for (const [outputIndex, output] of txData.outputs.entries()) { if (output.isChange) { changeInfo.push({ outputIndex, @@ -318,10 +322,13 @@ function SendTokens() { const useOldProtocol = !versionUtils.isLedgerCustomTokenAllowed(); const network = wallet.getNetworkObject(); - ledger.sendTx(newTxData, changeInfo, useOldProtocol, network); + + // Store the SendTransaction instance on the variable exclusive to Ledger event handlers + sendTransactionRef.current = sendTransactionObj; + ledger.sendTx(txData, changeInfo, useOldProtocol, network); globalModalContext.showModal(MODAL_TYPES.ALERT, { title: t`Validate outputs on Ledger`, - body: renderAlertBody(), + body: renderAlertBody(LEDGER_MODAL_STATE.WAITING_APPROVAL), showFooter: false, }); } @@ -331,13 +338,17 @@ function SendTokens() { * Checks if the form is valid, get data from child components, complete the transaction and execute API request */ const beforeSend = () => { - const isValid = validateData(); + // Validates form + const isValid = validateFormData(); if (!isValid) return; - let data = getData(); + + // Validates inputs and outputs + let data = getFormData(); if (!data) return; + + // All inputs validated: proceed with the send process setErrorMessage(''); try { - newTxData = data; if (!LOCAL_STORE.isHardwareWallet()) { globalModalContext.showModal(MODAL_TYPES.PIN, { onSuccess: ({pin}) => { @@ -366,15 +377,16 @@ function SendTokens() { * @return {SendTransaction} SendTransaction object, in case of success, null otherwise */ const prepareSendTransaction = async (pin) => { + const txData = getFormData(); if (useWalletService) { return new hathorLib.SendTransactionWalletService(wallet, { - outputs: newTxData.outputs, - inputs: newTxData.inputs, + outputs: txData.outputs, + inputs: txData.inputs, pin, }); } - return new hathorLib.SendTransaction({ outputs: newTxData.outputs, inputs: newTxData.inputs, pin, storage: wallet.storage }); + return new hathorLib.SendTransaction({ outputs: txData.outputs, inputs: txData.inputs, pin, storage: wallet.storage }); } /** @@ -389,7 +401,6 @@ function SendTokens() { e instanceof LedgerError) { globalModalContext.hideModal(); setErrorMessage(e.message); - setLedgerStep(0); } else { // Unhandled error throw e; @@ -492,21 +503,31 @@ function SendTokens() { return
    {rows}
} - const renderAlertBody = () => { - if (ledgerStep === 0) { + /** + * Renders the body for the ledger alert modal. + * Depending on the parameters, it can: + * - Render instructions on how to approve the tx on ledger or; + * - Trigger the transaction sending process and render a tx monitoring component + * @param {LEDGER_MODAL_STATE} modalState Defines how the modal will be rendered + * @param {SendTransaction} [sendTransactionObj] Reference to tx already signed by Ledger, that will be sent + */ + const renderAlertBody = (modalState, sendTransactionObj) => { + if (modalState === LEDGER_MODAL_STATE.WAITING_APPROVAL) { return (

{t`Please go to you Ledger and validate each output of your transaction. Press both buttons in case the output is correct.`}

{t`In the end, a final screen will ask you to confirm sending the transaction.`}

); - } else { + } else if (modalState === LEDGER_MODAL_STATE.LEDGER_APPROVED) { return (
- +
) + } else { + throw new Error('Invalid modal state'); } } diff --git a/src/screens/Wallet.js b/src/screens/Wallet.js index 1f2fd2ec..a2731fc7 100644 --- a/src/screens/Wallet.js +++ b/src/screens/Wallet.js @@ -269,6 +269,7 @@ function Wallet() { context.showModal(MODAL_TYPES.LEDGER_SIGN_TOKEN, { token, modalId: 'signTokenDataModal', + cb: () => () => {} // XXX: cb is a mandatory parameter for this modal }) } From 4f52cc6883f3fd4e4960003ea167321c0d1201af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 21 Mar 2024 18:07:16 -0300 Subject: [PATCH 06/10] docs: wallet-service qa (#532) * feat: initial wallet service qa * Update QA_WALLET_SERVICE.md Co-authored-by: Pedro Ferreira * docs: send tx with htr and custom token to wallet-service qa * chore: review changes --------- Co-authored-by: Pedro Ferreira --- QA_WALLET_SERVICE.md | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 QA_WALLET_SERVICE.md diff --git a/QA_WALLET_SERVICE.md b/QA_WALLET_SERVICE.md new file mode 100644 index 00000000..fd82d2e6 --- /dev/null +++ b/QA_WALLET_SERVICE.md @@ -0,0 +1,66 @@ +1. **Wallet Service initialization** + 1. Start a new wallet + 1. Send 1.00 HTR from another wallet to the wallet address. + 1. Create a custom token 'WST0', amount 100. + 1. Go to Settings and copy the "Unique identifier" + 1. Go to the unleash dashboard and add it to the list of UserIDs + 1. Close your wallet (not lock) and open it again. + 1. Unlock the wallet and check that your history is showing correctly. + 1. Check that you have balance for HTR (0.99 HTR) and WST0 (100.00 WST0) + 1. Check that you have authorities for mint and melt for WST0. + +1. **Wallet screen** + 1. Check that you have HTR and other custom tokens on your wallet. + 1. Copy the address and send 1.00 HTR from another wallet to this address. + 1. Check that we receive a notification for the transaction. + 1. Check that the transaction appears in the list, the balance was updated and the address also changed. + 1. Click on 'See all addresses' and see the list. + 1. Search for the address used to send the transaction and check that it has 'Number of transactions' equal to 1. + +1. **Custom tokens** + 1. Click on 'Custom tokens', then 'Create a new token'. + 1. Create a token 'WS Test Token', 'WTST', amount 100. + 1. Validate its symbol appeared selected in the token bar. + 1. The list of transactions should have only one, with type 'Token creation' and amount of 100.00. + 1. Click on the HTR token (in the token bar) and check if the first transaction is of type 'Token deposit' with amount of 1.00. + +1. **Custom token admin** + 1. Click on 'WTST' on the token bar and then on 'Administrative tools' + 1. Use the 'Melt tokens' to melt all tokens you have. + 1. Go to 'Balance & History' and check that the 'Total' is 0 and that the melt transaction appears on the list. + 1. Go back to 'Administrative tools' and use 'Mint tokens' mint 100 tokens. + 1. Click on 'Balance & History' and check the mint transaction is on the list. + 1. Check that the 'Total' and 'Available' are 100.00 WTST + 1. Copy the current address and go back to 'Administrative tools' + 1. Use the 'Delegate mint' to the address you copied, keep the 'Create another mint output for you?' checked. + 1. Check that the message under 'Mint authority management' now reads 'You are the owner of 2 mint outputs'. + 1. Use the 'Destroy mint' to destroy one authority. + 1. Check that the message under 'Mint authority management' now reads 'You are the owner of 1 mint output'. + 1. Use the 'Delegate melt' to the address you copied, keep the 'Create another mint output for you?' checked. + 1. Check that the message under 'Melt authority management' now reads 'You are the owner of 2 melt outputs'. + 1. Use the 'Destroy melt' to destroy one authority. + 1. Check that the message under 'Melt authority management' now reads 'You are the owner of 1 melt output'. + +1. **Settings** + 1. Go to Settings and click on "Change server" + 1. Use the following urls to connect to testnet wallet service. + 1. `https://wallet-service.testnet.hathor.network` + 1. `wss://ws.wallet-service.testnet.hathor.network` + 1. Check that you are connected to the testnet in the upper right corner. + 1. Check that your address starts with 'W'. + 1. Go to Settings and click on "Change server" + 1. Connect to the mainnet wallet service. + 1. `https://wallet-service.hathor.network` + 1. `wss://ws.wallet-service.hathor.network` + 1. Check that you are connected to the mainnet in the upper right corner. + 1. Check that your address starts with 'H'. + 1. Go to Settings and set "Hide zero-balance tokens" to yes. + 1. Go to "WTST" and melt all tokens. + 1. Check that "WTST" does not appear on the token bar. + 1. Go to Settings and set "Hide zero-balance tokens" to no. + 1. Check that "WTST" appears on the token bar. + 1. Click on "WTST" and go to "About WS Test Token" and change "Always show this token" to yes. + 1. Go to Settings and set "Hide zero-balance tokens" to yes. + 1. Melt all WTST tokens. + 1. Check that "WTST" is still on the token bar. + 1. Check that the "WTST" balance is zero. From 51312275b79b825fd7b7ba6999dc8a70bec87cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 25 Mar 2024 20:01:06 -0300 Subject: [PATCH 07/10] fix: hardware wallet migration (#554) * fix: clean localStorage for hardware migrations * feat: also persist app:uniqueId * chore: remove unnecessary flags --- public/preload.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/public/preload.js b/public/preload.js index 0fbb044c..d694d299 100644 --- a/public/preload.js +++ b/public/preload.js @@ -16,6 +16,21 @@ Sentry.init({ }) process.once('loaded', () => { + const oldAccessDataRaw = localStorage.getItem('wallet:accessData'); + if (oldAccessDataRaw) { + // When migrating from wallets with version v0.26.0 and older initialized with hardware devices + // we need to clean the localStorage since some state left over can cause issues + const accessData = JSON.parse(oldAccessDataRaw); + const isHardware = localStorage.getItem('wallet:type') === 'hardware'; + if (accessData.from_xpub || isHardware) { + const uniqueId = localStorage.getItem('app:uniqueId'); + localStorage.clear(); + localStorage.setItem('localstorage:started', 'true'); + localStorage.setItem('app:uniqueId', uniqueId); + } + } + + // Set closed in localStorage, so user does not open in the wallet page localStorage.setItem('localstorage:closed', 'true'); From e32475e5a637b6eff55da47436eab8ac69d42f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 26 Mar 2024 21:40:43 -0300 Subject: [PATCH 08/10] feat: reset data menu (#560) * feat: add menu to reset storage * feat: confirm reset data modal * feat: move reset menu into debug * chore: review changes * chore: review changes --- public/electron.js | 10 +++- src/App.js | 15 ++++- src/components/GlobalModal.js | 3 + src/components/ModalConfirmClearStorage.js | 68 ++++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/components/ModalConfirmClearStorage.js diff --git a/public/electron.js b/public/electron.js index 3b1d1c3a..ea25eaa6 100644 --- a/public/electron.js +++ b/public/electron.js @@ -115,14 +115,15 @@ function createWindow () { { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' } - ]} + ]}, ]; if (debugMode) { template.push({ label: 'Debug', submenu: [ - { label: `Open DevTools`, accelerator: 'CmdOrCtrl+B', click: function() { mainWindow.webContents.openDevTools(); }} + { label: `Open DevTools`, accelerator: 'CmdOrCtrl+B', click: function() { mainWindow.webContents.openDevTools(); }}, + { label: 'Reset all data', click: function() { mainWindow.webContents.send('app:clear_storage'); }}, ] }); }; @@ -167,6 +168,11 @@ function createWindow () { } }); + ipcMain.on('app:clear_storage_success', () => { + console.log('Data reset success. Closing window...'); + mainWindow.close(); + }); + addLedgerListeners(mainWindow); } diff --git a/src/App.js b/src/App.js index d9ea8628..257dc3fe 100644 --- a/src/App.js +++ b/src/App.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import { Redirect, Route, Switch, useHistory, useRouteMatch } from 'react-router-dom'; import Wallet from './screens/Wallet'; import SendTokens from './screens/SendTokens'; @@ -38,6 +38,7 @@ import tokensUtils from './utils/tokens'; import storageUtils from './utils/storage'; import { useDispatch, useSelector } from 'react-redux'; import RequestErrorModal from './components/RequestError'; +import { GlobalModalContext, MODAL_TYPES } from './components/GlobalModal'; import createRequestInstance from './api/axiosInstance'; import hathorLib from '@hathor/wallet-lib'; import { IPC_RENDERER } from './constants'; @@ -67,6 +68,7 @@ function Root() { }); const dispatch = useDispatch(); const history = useHistory(); + const context = useContext(GlobalModalContext); // Monitors when Ledger device loses connection or the app is closed useEffect(() => { @@ -98,6 +100,17 @@ function Root() { // If there is an `Inter Process Communication` channel available, initialize Ledger logic if (IPC_RENDERER) { + // Event called when the user wants to reset all data + IPC_RENDERER.on('app:clear_storage', async () => { + context.showModal(MODAL_TYPES.CONFIRM_CLEAR_STORAGE, { + success: () => { + localStorage.clear(); + IPC_RENDERER.send('app:clear_storage_success'); + }, + }); + + }); + // Registers the event handlers for the ledger // Event called when user quits hathor app diff --git a/src/components/GlobalModal.js b/src/components/GlobalModal.js index 394ccee9..76364402 100644 --- a/src/components/GlobalModal.js +++ b/src/components/GlobalModal.js @@ -18,6 +18,7 @@ import ModalLedgerResetTokenSignatures from './ModalLedgerResetTokenSignatures'; import ModalResetAllData from './ModalResetAllData'; import ModalLedgerSignToken from './ModalLedgerSignToken'; import ModalConfirmTestnet from './ModalConfirmTestnet'; +import ModalConfirmClearStorage from './ModalConfirmClearStorage'; import ModalSendTx from './ModalSendTx'; import ModalUnregisteredTokenInfo from './ModalUnregisteredTokenInfo'; import ModalPin from "./ModalPin"; @@ -43,6 +44,7 @@ export const MODAL_TYPES = { 'RESET_ALL_DATA': 'RESET_ALL_DATA', 'LEDGER_SIGN_TOKEN': 'LEDGER_SIGN_TOKEN', 'CONFIRM_TESTNET': 'CONFIRM_TESTNET', + 'CONFIRM_CLEAR_STORAGE': 'CONFIRM_CLEAR_STORAGE', 'SEND_TX': 'SEND_TX', 'UNREGISTERED_TOKEN_INFO': 'UNREGISTERED_TOKEN_INFO', 'PIN': 'PIN', @@ -63,6 +65,7 @@ export const MODAL_COMPONENTS = { [MODAL_TYPES.RESET_ALL_DATA]: ModalResetAllData, [MODAL_TYPES.LEDGER_SIGN_TOKEN]: ModalLedgerSignToken, [MODAL_TYPES.CONFIRM_TESTNET]: ModalConfirmTestnet, + [MODAL_TYPES.CONFIRM_CLEAR_STORAGE]: ModalConfirmClearStorage, [MODAL_TYPES.SEND_TX]: ModalSendTx, [MODAL_TYPES.UNREGISTERED_TOKEN_INFO]: ModalUnregisteredTokenInfo, [MODAL_TYPES.PIN]: ModalPin, diff --git a/src/components/ModalConfirmClearStorage.js b/src/components/ModalConfirmClearStorage.js new file mode 100644 index 00000000..073afa72 --- /dev/null +++ b/src/components/ModalConfirmClearStorage.js @@ -0,0 +1,68 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { t } from 'ttag'; +import $ from 'jquery'; +import SpanFmt from './SpanFmt'; +import { CONFIRM_RESET_MESSAGE } from '../constants'; + +export default function ModalConfirmClearStorage({ onClose, success }) { + const [confirmText, setConfirmText] = useState(''); + const [confirmError, setConfirmError] = useState(''); + + useEffect(() => { + $('#modalConfirmResetAllData').modal('show'); + $('#modalConfirmResetAllData').on('hidden.bs.modal', onClose); + + return () => { + $('#modalConfirmResetAllData').modal('hide'); + $('#modalConfirmResetAllData').off(); + }; + }, []); + + const confirmResetData = useCallback(() => { + if (confirmText.toLowerCase() !== CONFIRM_RESET_MESSAGE.toLowerCase()) { + setConfirmError(t`Invalid value.`); + return; + } + + success(); + }, [confirmText, setConfirmError]); + + return ( + + ); +} From abb4da2d3146e5394754f22881b35afe0ce895e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 27 Mar 2024 12:43:55 -0300 Subject: [PATCH 09/10] docs: hardware wallet migration qa (#562) * docs: hardware wallet migration qa * chore: remove useless link * docs: check the reset menu on the QA --- QA.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/QA.md b/QA.md index 2367cee4..14a92b34 100644 --- a/QA.md +++ b/QA.md @@ -205,8 +205,21 @@ You can connect your wallet to the testnet (https://node1.foxtrot.testnet.hathor 1. Close and open the wallet again and start a new wallet one without doing backup. It must show a yellow warning saying a backup must be done. 1. Do the backup (following procedures in the 'Initialization' tests). The backup message has to disappear. +1. **Reset menu** + 1. Click on the application menu Debug > Reset all data. Then fill the form with "anything" and click on the "Reset all data" button. + 1. Check that a message with "Invalid value." appears. + 1. Click on "Cancel", the modal should close. + 1. Click on the application menu Debug > Reset all data. Then fill the form with "I want to reset my wallet" and click on the "Reset all data" button. The wallet should close. + 1. Open the wallet again, it should open the Welcome screen. Do NOT click on "Get started". + 1. Close the wallet. + +1. **Hardware wallet migration** + 1. Uninstall the wallet and install the previous version. + 1. Connect your Ledger device to the computer. The Ledger should be initialized already. + 1. Start the wallet with the Ledger device and copy the current address. + 1. Close the wallet and install the latest version. + 1. **Hardware wallet initialization** - 1. Reset the wallet one more time. 1. Connect your Ledger device to the computer. The Ledger should be initialized already. 1. Open the Hathor app on Ledger. 1. On the desktop wallet, mark the checkbox and click 'Get Started'. @@ -216,6 +229,7 @@ You can connect your wallet to the testnet (https://node1.foxtrot.testnet.hathor 1. Deny the authorization request on Ledger (scroll with left or right and click both buttons on "Reject" step). It should show an error on the wallet. 1. Click 'Try again'. It goes through both steps and asks for authorization again. 1. Grant authorization request (click both buttons on the "Approve" step). It will proceed to 'Loading transactions' screen. + 1. Once the wallet is loaded check that the current address match the copied address during "**Hardware wallet migration**" 1. **Ledger wallet screen** 1. On the main wallet screen, confirm 'Address to receive tokens' is truncated (eg: 'HGZmqThwa6...'). There should be a 'Show full address' button next to it. From d86bd2b62adafabd4c6848430f057a463ba4fc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 27 Mar 2024 15:23:55 -0300 Subject: [PATCH 10/10] chore: bump v0.28.0-rc1 (#561) --- package-lock.json | 2 +- package.json | 2 +- public/electron.js | 2 +- src/constants.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff595a0a..159f6339 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.27.1-rc5", + "version": "0.28.0-rc1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 46609ce9..4bbdcd61 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "productName": "Hathor Wallet", "description": "Light wallet for Hathor Network", "author": "Hathor Labs (https://hathor.network/)", - "version": "0.27.1-rc5", + "version": "0.28.0-rc1", "engines": { "node": ">=20.0.0", "npm": ">=10.0.0" diff --git a/public/electron.js b/public/electron.js index ea25eaa6..d2de77c7 100644 --- a/public/electron.js +++ b/public/electron.js @@ -35,7 +35,7 @@ if (process.platform === 'darwin') { } const appName = 'Hathor Wallet'; -const walletVersion = '0.27.1-rc5'; +const walletVersion = '0.28.0-rc1'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/constants.js b/src/constants.js index 0e2d38c6..b3daf4b5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -21,7 +21,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.27.1-rc5'; +export const VERSION = '0.28.0-rc1'; /** * Before this version the data in localStorage from the wallet is not compatible