From d20f201c512646341cc6ed39b08c0d8e4963c4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Thu, 13 Apr 2023 14:20:22 -0300 Subject: [PATCH 01/23] refactor: feature toggle is now a saga (#399) * feat: added feature toggle saga * feat: added atomic swap feature toggle check * chore: removed featureFlag file * feat: update polling interval to 12s * feat: properly setting/removing ignore ws feature toggle * refactor: removed duplicated code * fix: properly resetting without cancelling unleash * feat: listen for wallet service disable and reload * docs: added docstring for helper methods * refactor: removed unused method * docs: added return docstring --- .envrc | 7 + .gitignore | 2 + flake.lock | 92 +++++++++++ flake.nix | 24 +++ src/actions/index.js | 33 ++++ src/components/ModalUnhandledError.js | 12 +- src/constants.js | 12 +- src/featureFlags.js | 144 ----------------- src/reducers/index.js | 44 ++++- src/sagas/featureToggle.js | 224 ++++++++++++++++++++++++++ src/sagas/helpers.js | 38 ++++- src/sagas/index.js | 2 + src/sagas/wallet.js | 142 ++++++++-------- src/screens/LockedWallet.js | 8 +- src/screens/Settings.js | 11 +- src/screens/WalletVersionError.js | 7 +- src/utils/wallet.js | 18 +-- 17 files changed, 574 insertions(+), 246 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 src/featureFlags.js create mode 100644 src/sagas/featureToggle.js diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..7a65628c --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +if [[ $(type -t use_flake) != function ]]; then + echo "ERROR: use_flake function missing." + echo "Please update direnv to v2.30.0 or later." + exit 1 +fi + +use flake diff --git a/.gitignore b/.gitignore index 95c91a59..afb7b534 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# flake +.direnv/ *.swp *.swo diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..1c73b027 --- /dev/null +++ b/flake.lock @@ -0,0 +1,92 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1650201426, + "narHash": "sha256-u43Xf03ImFJWKLHddtjOpCJCHbuM0SQbb6FKR5NuFhk=", + "owner": "numtide", + "repo": "devshell", + "rev": "cb76bc75a0ee81f2d8676fe681f268c48dbd380e", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1642700792, + "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1649676176, + "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1643381941, + "narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1650194139, + "narHash": "sha256-kurZsqeOw5fpqA/Ig+8tHvbjwzs5P9AE6WUKOX1m6qM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bd4dffcdb7c577d74745bd1eff6230172bd176d5", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..9d2f2498 --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "virtual environments"; + + inputs.devshell.url = "github:numtide/devshell"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, flake-utils, devshell, nixpkgs }: + + flake-utils.lib.eachDefaultSystem (system: { + devShell = + let pkgs = import nixpkgs { + inherit system; + + overlays = [ devshell.overlay ]; + }; + in + pkgs.devshell.mkShell { + packages = with pkgs; [ + nixpkgs-fmt + nodejs-14_x + ]; + }; + }); +} diff --git a/src/actions/index.js b/src/actions/index.js index e1cb48eb..5530a91d 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -40,10 +40,15 @@ export const types = { WALLET_STATE_READY: 'WALLET_STATE_READY', WALLET_STATE_ERROR: 'WALLET_STATE_ERROR', WALLET_RELOAD_DATA: 'WALLET_RELOAD_DATA', + WALLET_RESET: 'WALLET_RESET', + WALLET_RESET_SUCCESS: 'WALLET_RESET_SUCCESS', WALLET_REFRESH_SHARED_ADDRESS: 'WALLET_REFRESH_SHARED_ADDRESS', SET_SERVER_INFO: 'SET_SERVER_INFO', STORE_ROUTER_HISTORY: 'STORE_ROUTER_HISTORY', WALLET_RELOADING: 'WALLET_RELOADING', + FEATURE_TOGGLE_INITIALIZED: 'FEATURE_TOGGLE_INITIALIZED', + SET_FEATURE_TOGGLES: 'SET_FEATURE_TOGGLES', + SET_UNLEASH_CLIENT: 'SET_UNLEASH_CLIENT', }; /** @@ -429,3 +434,31 @@ export const setServerInfo = ({ version, network }) => ( { type: types.SET_SERVER_INFO, payload: { version, network } } ); +export const featureToggleInitialized = () => ({ + type: types.FEATURE_TOGGLE_INITIALIZED, +}); + +/** + * toggles {Object} Key value object where the key is the feature toggle name and the value + * indicates whether it is on (true) or off (false) + */ +export const setFeatureToggles = (toggles) => ({ + type: types.SET_FEATURE_TOGGLES, + payload: toggles, +}); + +/** + * unleashClient {UnleashClient} The unleash client to store + */ +export const setUnleashClient = (unleashClient) => ({ + type: types.SET_UNLEASH_CLIENT, + payload: unleashClient, +}); + +export const walletResetSuccess = () => ({ + type: types.WALLET_RESET_SUCCESS, +}); + +export const walletReset = () => ({ + type: types.WALLET_RESET, +}); diff --git a/src/components/ModalUnhandledError.js b/src/components/ModalUnhandledError.js index de14034b..2673c44a 100644 --- a/src/components/ModalUnhandledError.js +++ b/src/components/ModalUnhandledError.js @@ -9,6 +9,14 @@ import React from 'react'; import { t } from 'ttag'; import wallet from '../utils/wallet'; import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { walletReset } from '../actions'; +import { connect } from 'react-redux'; + +const mapDispatchToProps = dispatch => { + return { + walletReset: () => dispatch(walletReset()), + }; +}; /** * Component that shows a modal when an unhandled error happens @@ -29,7 +37,7 @@ class ModalUnhandledError extends React.Component { */ handleConfirm = (e) => { e.preventDefault(); - wallet.resetWalletData(); + this.props.walletReset(); this.props.history.push('/welcome/'); } @@ -98,4 +106,4 @@ class ModalUnhandledError extends React.Component { } } -export default ModalUnhandledError; +export default connect(null, mapDispatchToProps)(ModalUnhandledError); diff --git a/src/constants.js b/src/constants.js index 06efdca5..6c3b29db 100644 --- a/src/constants.js +++ b/src/constants.js @@ -255,10 +255,20 @@ export const DEFAULT_WALLET_SERVICE_WS_SERVERS = [ */ export const UNLEASH_URL = 'https://unleash-proxy.b7e6a7f52ee9fefaf0c53e300cfcb014.hathor.network/proxy'; export const UNLEASH_CLIENT_KEY = 'wKNhpEXKa39aTRgIjcNsO4Im618bRGTq'; -export const UNLEASH_POLLING_INTERVAL = 120; // seconds +export const UNLEASH_POLLING_INTERVAL = 12 * 1000; // 12s + +/** + * Flag name stored in localStorage to ignore the FeatureToggle for wallet service + */ +export const IGNORE_WS_TOGGLE_FLAG = 'featureFlags:ignoreWalletServiceFlag'; /** * The feature toggle configured in Unleash */ export const WALLET_SERVICE_FEATURE_TOGGLE = 'wallet-service-desktop.rollout'; export const ATOMIC_SWAP_SERVICE_FEATURE_TOGGLE = 'atomic-swap-service-desktop.rollout'; + +export const FEATURE_TOGGLE_DEFAULTS = { + [WALLET_SERVICE_FEATURE_TOGGLE]: false, + [ATOMIC_SWAP_SERVICE_FEATURE_TOGGLE]: false, +}; diff --git a/src/featureFlags.js b/src/featureFlags.js deleted file mode 100644 index 559c18ed..00000000 --- a/src/featureFlags.js +++ /dev/null @@ -1,144 +0,0 @@ -import events from 'events'; -import { UnleashClient } from 'unleash-proxy-client'; -import { - UNLEASH_URL, - UNLEASH_CLIENT_KEY, - UNLEASH_POLLING_INTERVAL, - WALLET_SERVICE_FEATURE_TOGGLE, - ATOMIC_SWAP_SERVICE_FEATURE_TOGGLE, -} from './constants'; -import helpers from './utils/helpers'; - -const IGNORE_WALLET_SERVICE_FLAG = 'featureFlags:ignoreWalletServiceFlag'; - -export const Events = { - WALLET_SERVICE_ENABLED: 'wallet-service-enabled', - ATOMIC_SWAP_ENABLED: 'atomic-swap-enabled', -}; - -export class FeatureFlags extends events.EventEmitter { - constructor(userId, network) { - super(); - - this.userId = userId; - this.network = network; - this.walletServiceFlag = WALLET_SERVICE_FEATURE_TOGGLE; - this.atomicSwapFlag = ATOMIC_SWAP_SERVICE_FEATURE_TOGGLE; - this.walletServiceEnabled = null; - this.atomicSwapEnabled = null; - this.client = new UnleashClient({ - url: UNLEASH_URL, - clientKey: UNLEASH_CLIENT_KEY, - refreshInterval: UNLEASH_POLLING_INTERVAL, - appName: `wallet-service-wallet-desktop`, - }); - - this.client.on('update', () => { - // Get current flag - const walletServiceEnabled = this.client.isEnabled(this.walletServiceFlag); - const atomicSwapEnabled = this.client.isEnabled(this.atomicSwapFlag); - - // We should only emit an update if we already had a value on the instance - // and if the value has changed - if (this.walletServiceEnabled !== null && ( - this.walletServiceEnabled !== walletServiceEnabled - )) { - this.walletServiceEnabled = walletServiceEnabled; - this.emit(Events.WALLET_SERVICE_ENABLED, walletServiceEnabled); - } - - if (this.atomicSwapEnabled !== null && ( - this.atomicSwapEnabled !== atomicSwapEnabled - )) { - this.atomicSwapEnabled = atomicSwapEnabled; - this.emit(Events.ATOMIC_SWAP_ENABLED, atomicSwapEnabled); - } - }); - } - - /** - * Uses the Hathor Unleash Server and Proxy to determine if the - * wallet should use the WalletService facade or the old facade - * - * @params {string} userId An user identifier (e.g. the firstAddress) - * @params {string} network The network name ('mainnet' or 'testnet') - * - * @return {boolean} The result from the unleash feature flag - */ - async shouldUseWalletService() { - try { - const shouldIgnore = await localStorage.getItem(IGNORE_WALLET_SERVICE_FLAG); - if (shouldIgnore) { - return false; - } - this.client.updateContext({ - userId: this.userId, - properties: { - network: this.network, - platform: helpers.getCurrentOS(), - }, - }); - - // Start polling for feature flag updates - await this.client.start(); - - // start() method will have already called the fetchToggles, so the flag should be enabled - const isWalletServiceEnabled = this.client.isEnabled(this.walletServiceFlag); - this.walletServiceEnabled = isWalletServiceEnabled; - - return this.walletServiceEnabled; - } catch (e) { - // If our feature flag service is unavailable, we default to the - // old facade - return false; - } - } - - /** - * Sets the ignore flag on the storage to persist it between app restarts - */ - async ignoreWalletServiceFlag() { - await localStorage.setItem(IGNORE_WALLET_SERVICE_FLAG, 'true'); - this.walletServiceEnabled = false; - - // Stop the client from polling - this.client.stop(); - } - - /** - * Removes the ignore flag from the storage - */ - static async clearIgnoreWalletServiceFlag() { - await localStorage.removeItem(IGNORE_WALLET_SERVICE_FLAG); - } - - /** - * Uses the Hathor Unleash Server and Proxy to determine if the - * wallet should have the Atomic Swap feature - * - * @return {boolean} The result from the unleash feature flag - */ - async isAtomicSwapEnabled() { - try { - await this.client.updateContext({ - userId: this.userId, - properties: { - network: this.network, - platform: helpers.getCurrentOS(), - }, - }); - - // Start polling for feature flag updates - await this.client.start(); - - // start() method will have already called the fetchToggles, so the flag should be enabled - this.atomicSwapEnabled = this.client.isEnabled(this.atomicSwapFlag); - - return this.atomicSwapEnabled; - } catch (e) { - // If unleash is unavailable, this is the fallback result - // XXX: After the Atomic Swap feature is released, this should be changed to `true` - return false; - } - } -} diff --git a/src/reducers/index.js b/src/reducers/index.js index 45330839..a9c32b22 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -11,8 +11,9 @@ import { types } from '../actions'; import { get } from 'lodash'; import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; import { WALLET_STATUS } from '../sagas/wallet'; -import { PROPOSAL_DOWNLOAD_STATUS } from "../utils/atomicSwap"; +import { PROPOSAL_DOWNLOAD_STATUS } from '../utils/atomicSwap'; import { HATHOR_TOKEN_CONFIG } from "@hathor/wallet-lib/lib/constants"; +import { FEATURE_TOGGLE_DEFAULTS } from '../constants'; /** * @typedef TokenHistory @@ -126,6 +127,11 @@ const initialState = { status: TOKEN_DOWNLOAD_STATUS.READY } }, + unleashClient: null, + featureTogglesInitialized: false, + featureToggles: { + ...FEATURE_TOGGLE_DEFAULTS, + }, }; const rootReducer = (state = initialState, action) => { @@ -250,6 +256,14 @@ const rootReducer = (state = initialState, action) => { return onWalletBestBlockUpdate(state, action); case types.STORE_ROUTER_HISTORY: return onStoreRouterHistory(state, action); + case types.SET_UNLEASH_CLIENT: + return onSetUnleashClient(state, action); + case types.SET_FEATURE_TOGGLES: + return onSetFeatureToggles(state, action); + case types.FEATURE_TOGGLE_INITIALIZED: + return onFeatureToggleInitialized(state); + case types.WALLET_RESET_SUCCESS: + return onWalletResetSuccess(state); default: return state; } @@ -962,4 +976,32 @@ const onSetServerInfo = (state, action) => ({ }, }); +const onFeatureToggleInitialized = (state) => ({ + ...state, + featureTogglesInitialized: true, +}); + +/** + * @param {Object} action.payload The key->value object with feature toggles + */ +const onSetFeatureToggles = (state, { payload }) => ({ + ...state, + featureToggles: payload, +}); + +/** + * @param {Object} action.payload The unleash client to store + */ +const onSetUnleashClient = (state, { payload }) => ({ + ...state, + unleashClient: payload, +}); + +const onWalletResetSuccess = (state) => ({ + ...state, + // Keep the unleashClient as it should continue running + unleashClient: state.unleashClient, +}); + + export default rootReducer; diff --git a/src/sagas/featureToggle.js b/src/sagas/featureToggle.js new file mode 100644 index 00000000..fb2a001f --- /dev/null +++ b/src/sagas/featureToggle.js @@ -0,0 +1,224 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + UnleashClient, + EVENTS as UnleashEvents, +} from 'unleash-proxy-client'; +import { get } from 'lodash'; +import { + takeEvery, + all, + call, + delay, + put, + cancelled, + select, + race, + take, + fork, + spawn, +} from 'redux-saga/effects'; +import { eventChannel } from 'redux-saga'; +import { config } from '@hathor/wallet-lib'; +import helpers from '../utils/helpers'; + +import { + setUnleashClient, + setFeatureToggles, + featureToggleInitialized, +} from '../actions'; +import { + UNLEASH_URL, + UNLEASH_CLIENT_KEY, + UNLEASH_POLLING_INTERVAL, + FEATURE_TOGGLE_DEFAULTS, +} from '../constants'; + +const CONNECT_TIMEOUT = 10000; +const MAX_RETRIES = 5; + +export function* handleInitFailed(currentRetry) { + if (currentRetry >= MAX_RETRIES) { + console.error('Max retries reached while trying to create the unleash-proxy client.'); + const unleashClient = yield select((state) => state.unleashClient); + + if (unleashClient) { + unleashClient.close(); + yield put(setUnleashClient(null)); + } + + // Even if unleash failed, we should allow the app to continue as it + // has defaults set for all feature toggles. Emit featureToggleInitialized + // so sagas waiting for it will resume. + yield put(featureToggleInitialized()); + return; + } + + yield spawn(monitorFeatureFlags, currentRetry + 1); +} + +export function* fetchTogglesRoutine() { + while (true) { + // Wait first so we don't double-check on initialization + yield delay(UNLEASH_POLLING_INTERVAL); + + const unleashClient = yield select((state) => state.unleashClient); + + try { + yield call(() => unleashClient.fetchToggles()); + } catch (e) { + // No need to do anything here as it will try again automatically in + // UNLEASH_POLLING_INTERVAL. Just prevent it from crashing the saga. + console.error('Erroed fetching feature toggles', e); + } + } +} + +export function* monitorFeatureFlags(currentRetry = 0) { + const unleashClient = new UnleashClient({ + url: UNLEASH_URL, + clientKey: UNLEASH_CLIENT_KEY, + refreshInterval: -1, + disableRefresh: true, // Disable it, we will handle it ourselves + appName: 'wallet-desktop', + }); + + const userId = helpers.getUniqueId(); + const platform = helpers.getCurrentOS(); + const network = config.getNetwork().name; + + const options = { + userId, + properties: { + network, + platform, + }, + }; + + try { + yield call(() => unleashClient.updateContext(options)); + yield put(setUnleashClient(unleashClient)); + + // Listeners should be set before unleashClient.start so we don't miss + // updates + yield fork(setupUnleashListeners, unleashClient); + + // Start without awaiting it so we can listen for the + // READY event + unleashClient.start(); + + const { error, timeout } = yield race({ + error: take('FEATURE_TOGGLE_ERROR'), + success: take('FEATURE_TOGGLE_READY'), + timeout: delay(CONNECT_TIMEOUT), + }); + + if (error || timeout) { + throw new Error('Error or timeout while connecting to unleash proxy.'); + } + + // Fork the routine to download toggles. + yield fork(fetchTogglesRoutine); + + // At this point, unleashClient.start() already fetched the toggles + const featureToggles = mapFeatureToggles(unleashClient.toggles); + + yield put(setFeatureToggles(featureToggles)); + yield put(featureToggleInitialized()); + + if (yield cancelled()) { + yield call(() => unleashClient.stop()); + } + } catch (e) { + console.error('Error initializing unleash'); + unleashClient.stop(); + + yield put(setUnleashClient(null)); + + // Wait 500ms before retrying + yield delay(500); + + // Spawn so it's detached from the current thread + yield spawn(handleInitFailed, currentRetry); + } +} + +export function* setupUnleashListeners(unleashClient) { + const channel = eventChannel((emitter) => { + const listener = (state) => emitter(state); + + unleashClient.on(UnleashEvents.UPDATE, () => emitter({ + type: 'FEATURE_TOGGLE_UPDATE', + })); + + unleashClient.on(UnleashEvents.READY, () => emitter({ + type: 'FEATURE_TOGGLE_READY', + })); + + unleashClient.on(UnleashEvents.ERROR, (err) => emitter({ + type: 'FEATURE_TOGGLE_ERROR', + data: err, + })); + + return () => { + unleashClient.removeListener(UnleashEvents.UPDATE, listener); + unleashClient.removeListener(UnleashEvents.READY, listener); + unleashClient.removeListener(UnleashEvents.ERROR, listener); + }; + }); + + try { + while (true) { + const message = yield take(channel); + + yield put({ + type: message.type, + payload: message.data, + }); + } + } finally { + if (yield cancelled()) { + // When we close the channel, it will remove the event listener + channel.close(); + } + } +} + +function mapFeatureToggles(toggles) { + return toggles.reduce((acc, toggle) => { + acc[toggle.name] = get( + toggle, + 'enabled', + FEATURE_TOGGLE_DEFAULTS[toggle.name] || false, + ); + + return acc; + }, {}); +} + +export function* handleToggleUpdate() { + const unleashClient = yield select((state) => state.unleashClient); + const featureTogglesInitialized = yield select((state) => state.featureTogglesInitialized); + + if (!unleashClient || !featureTogglesInitialized) { + return; + } + + const { toggles } = unleashClient; + const featureToggles = mapFeatureToggles(toggles); + + yield put(setFeatureToggles(featureToggles)); + yield put({ type: 'FEATURE_TOGGLE_UPDATED' }); +} + +export function* saga() { + yield all([ + fork(monitorFeatureFlags), + takeEvery('FEATURE_TOGGLE_UPDATE', handleToggleUpdate), + ]); +} diff --git a/src/sagas/helpers.js b/src/sagas/helpers.js index 55f68424..62fe7e8c 100644 --- a/src/sagas/helpers.js +++ b/src/sagas/helpers.js @@ -1,5 +1,41 @@ import { get } from 'lodash'; -import { put, call, race, take } from 'redux-saga/effects'; +import { + put, + call, + race, + take, + select, +} from 'redux-saga/effects'; +import { types } from '../actions'; +import { FEATURE_TOGGLE_DEFAULTS } from '../constants'; + +/** + * Waits until feature toggle saga finishes loading + */ +export function* waitForFeatureToggleInitialization() { + const featureTogglesInitialized = yield select((state) => state.featureTogglesInitialized); + + if (!featureTogglesInitialized) { + // Wait until featureToggle saga completed initialization, which includes + // downloading the current toggle status for this client. + yield take(types.FEATURE_TOGGLE_INITIALIZED); + } +} + +/** + * This generator will wait until the feature toggle saga finishes loading and + * checks if a given flag is active + * + * @param {String} flag - The flag to check + * @return {Boolean} Whether the flag is on of off + */ +export function* checkForFeatureFlag(flag) { + yield call(waitForFeatureToggleInitialization); + + const featureToggles = yield select((state) => state.featureToggles); + + return get(featureToggles, flag, FEATURE_TOGGLE_DEFAULTS[flag] || false); +} /** * Helper method to be used on take saga effect, will wait until an action diff --git a/src/sagas/index.js b/src/sagas/index.js index 275f7837..72f04e51 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -2,12 +2,14 @@ import { all, fork } from 'redux-saga/effects'; import { saga as walletSagas } from './wallet'; import { saga as tokensSagas } from './tokens'; import { saga as proposalsSagas } from './atomicSwap'; +import { saga as featureToggleSagas } from './featureToggle'; function* defaultSaga() { yield all([ fork(walletSagas), fork(tokensSagas), fork(proposalsSagas), + fork(featureToggleSagas), ]); } diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index 1d2d8748..ddd62691 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -27,11 +27,10 @@ import STORE from '../storageInstance'; import { WALLET_SERVICE_MAINNET_BASE_WS_URL, WALLET_SERVICE_MAINNET_BASE_URL, + WALLET_SERVICE_FEATURE_TOGGLE, + ATOMIC_SWAP_SERVICE_FEATURE_TOGGLE, + IGNORE_WS_TOGGLE_FLAG, } from '../constants'; -import { - FeatureFlags, - Events as FeatureFlagEvents, -} from '../featureFlags'; import { types, isOnlineUpdate, @@ -51,16 +50,20 @@ import { walletStateError, walletStateReady, storeRouterHistory, - reloadWalletRequested, reloadingWallet, tokenInvalidateHistory, sharedAddressUpdate, walletRefreshSharedAddress, setEnableAtomicSwap, + walletResetSuccess, + reloadWalletRequested, } from '../actions'; -import { specificTypeAndPayload, errorHandler } from './helpers'; +import { + specificTypeAndPayload, + errorHandler, + checkForFeatureFlag, +} from './helpers'; import { fetchTokenData } from './tokens'; -import walletHelpers from '../utils/helpers'; import walletUtils from '../utils/wallet'; export const WALLET_STATUS = { @@ -69,6 +72,26 @@ export const WALLET_STATUS = { LOADING: 'loading', }; +export function* isWalletServiceEnabled() { + const shouldIgnoreFlag = localStorage.getItem(IGNORE_WS_TOGGLE_FLAG); + + // If we should ignore flag, it shouldn't matter what the featureToggle is, wallet service + // is definitely disabled. + if (shouldIgnoreFlag) { + return false; + } + + const walletServiceEnabled = yield call(checkForFeatureFlag, WALLET_SERVICE_FEATURE_TOGGLE); + + return walletServiceEnabled; +} + +export function* isAtomicSwapEnabled() { + const atomicSwapEnabled = yield call(checkForFeatureFlag, ATOMIC_SWAP_SERVICE_FEATURE_TOGGLE); + + return atomicSwapEnabled; +} + export function* startWallet(action) { const { words, @@ -95,13 +118,11 @@ export function* startWallet(action) { // We are offline, the connection object is yet to be created yield put(isOnlineUpdate({ isOnline: false })); - const uniqueDeviceId = walletHelpers.getUniqueId(); - const featureFlags = new FeatureFlags(uniqueDeviceId, network.name); const hardwareWallet = oldWalletUtil.isHardwareWallet(); // For now, the wallet service does not support hardware wallet, so default to the old facade - const useWalletService = hardwareWallet ? false : yield call(() => featureFlags.shouldUseWalletService()); - const enableAtomicSwap = yield call(() => featureFlags.isAtomicSwapEnabled()); + const useWalletService = hardwareWallet ? false : yield call(isWalletServiceEnabled); + const enableAtomicSwap = yield call(isAtomicSwapEnabled); yield put(setUseWalletService(useWalletService)); yield put(setEnableAtomicSwap(enableAtomicSwap)); @@ -183,22 +204,14 @@ export function* startWallet(action) { yield put(setWallet(wallet)); // Setup listeners before starting the wallet so we don't lose messages - const walletListenerThread = yield fork(setupWalletListeners, wallet); - - // Create a channel to listen for the ready state and - // wait until the wallet is ready - const walletReadyThread = yield fork(listenForWalletReady, wallet); + yield fork(setupWalletListeners, wallet); // Thread to listen for feature flags from Unleash - const featureFlagsThread = yield fork(listenForFeatureFlags, featureFlags); + yield fork(featureToggleUpdateListener); - // Keep track of the forked threads so we can cancel them later. We are currently - // using this to start the startWallet saga again during a reload - const threads = [ - walletListenerThread, - walletReadyThread, - featureFlagsThread - ]; + // Create a channel to listen for the ready state and + // wait until the wallet is ready + yield fork(listenForWalletReady, wallet); try { const serverInfo = yield call(wallet.start.bind(wallet), { @@ -224,11 +237,7 @@ export function* startWallet(action) { // the service is 'error' or if the start wallet request failed. // We should fallback to the old facade by storing the flag to ignore // the feature flag - yield call(featureFlags.ignoreWalletServiceFlag.bind(featureFlags)); - - // Cleanup all listeners - yield cancel(threads); - + localStorage.setItem(IGNORE_WS_TOGGLE_FLAG, true) // Yield the same action so it will now load on the old facade yield put(action); @@ -250,7 +259,6 @@ export function* startWallet(action) { if (error) { yield put(startWalletFailed()); - yield cancel(threads); return; } } @@ -261,7 +269,6 @@ export function* startWallet(action) { yield put(loadWalletSuccess(allTokens)); } catch(e) { yield put(startWalletFailed()); - yield cancel(threads); return; } @@ -281,9 +288,6 @@ export function* startWallet(action) { reload: take(types.RELOAD_WALLET_REQUESTED), }); - // We need to cancel threads on both reload and start - yield cancel(threads); - if (reload) { // Yield the same action again to reload the wallet yield put(action); @@ -380,41 +384,6 @@ export function* fetchTokensMetadata(tokens) { yield put(tokenMetadataUpdated(tokenMetadatas, errors)); } -// This will create a channel to listen for featureFlag updates -export function* listenForFeatureFlags(featureFlags) { - const channel = eventChannel((emitter) => { - const listener = (state) => emitter(state); - featureFlags.on(FeatureFlagEvents.WALLET_SERVICE_ENABLED, (state) => { - emitter(state); - }); - featureFlags.on(FeatureFlagEvents.ATOMIC_SWAP_ENABLED, (state) => { - emitter(state); - }); - - // Cleanup when the channel is closed - return () => { - featureFlags.removeListener(FeatureFlagEvents.WALLET_SERVICE_ENABLED, listener); - featureFlags.removeListener(FeatureFlagEvents.ATOMIC_SWAP_ENABLED, listener); - }; - }); - - try { - while (true) { - const newUseWalletService = yield take(channel); - const oldUseWalletService = yield select((state) => state.useWalletService); - - if (oldUseWalletService && oldUseWalletService !== newUseWalletService) { - yield put(reloadWalletRequested()); - } - } - } finally { - if (yield cancelled()) { - // When we close the channel, it will remove the event listener - channel.close(); - } - } -} - // This will create a channel from an EventEmitter to wait until the wallet is loaded, // dispatching actions export function* listenForWalletReady(wallet) { @@ -658,11 +627,46 @@ export function* refreshSharedAddress() { })); } +export function* onWalletReset() { + const routerHistory = yield select((state) => state.routerHistory); + + localStorage.removeItem(IGNORE_WS_TOGGLE_FLAG); + oldWalletUtil.resetWalletData(); + + yield put(walletResetSuccess()); + + routerHistory.push('/welcome'); +} + +export function* onWalletServiceDisabled() { + console.debug('We are currently in the wallet-service and the feature-flag is disabled, reloading.'); + yield put(reloadWalletRequested()); +} + +/** + * This saga will wait for feature toggle updates and react when a toggle state + * transition is done + */ +export function* featureToggleUpdateListener() { + while (true) { + yield take('FEATURE_TOGGLE_UPDATED'); + + const oldWalletServiceToggle = yield select(({ useWalletService }) => useWalletService); + const newWalletServiceToggle = yield call(isWalletServiceEnabled); + + // WalletService is currently ON and the featureToggle is now OFF + if (!newWalletServiceToggle && oldWalletServiceToggle) { + yield call(onWalletServiceDisabled); + } + } +} + export function* saga() { yield all([ takeLatest(types.START_WALLET_REQUESTED, errorHandler(startWallet, startWalletFailed())), takeLatest('WALLET_CONN_STATE_UPDATE', onWalletConnStateUpdate), takeLatest('WALLET_RELOADING', walletReloading), + takeLatest('WALLET_RESET', onWalletReset), takeEvery('WALLET_NEW_TX', handleTx), takeEvery('WALLET_UPDATE_TX', handleTx), takeEvery('WALLET_BEST_BLOCK_UPDATE', bestBlockUpdate), diff --git a/src/screens/LockedWallet.js b/src/screens/LockedWallet.js index b5972ff5..2596ec26 100644 --- a/src/screens/LockedWallet.js +++ b/src/screens/LockedWallet.js @@ -8,14 +8,12 @@ import React from 'react'; import { t } from 'ttag'; import { connect } from "react-redux"; -import ModalResetAllData from '../components/ModalResetAllData'; -import $ from 'jquery'; import wallet from '../utils/wallet'; import RequestErrorModal from '../components/RequestError'; import hathorLib from '@hathor/wallet-lib'; import ReactLoading from 'react-loading'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; -import { resolveLockWalletPromise, startWalletRequested } from '../actions'; +import { resolveLockWalletPromise, startWalletRequested, walletReset } from '../actions'; import colors from '../index.scss'; @@ -29,6 +27,7 @@ const mapDispatchToProps = dispatch => { return { resolveLockWalletPromise: pin => dispatch(resolveLockWalletPromise(pin)), startWallet: (payload) => dispatch(startWalletRequested(payload)), + walletReset: () => dispatch(walletReset()), }; }; @@ -122,8 +121,7 @@ class LockedWallet extends React.Component { */ handleReset = () => { this.context.hideModal(); - - wallet.resetWalletData(); + this.props.walletReset(); this.props.history.push('/welcome/'); } diff --git a/src/screens/Settings.js b/src/screens/Settings.js index d5885473..edd12bf2 100644 --- a/src/screens/Settings.js +++ b/src/screens/Settings.js @@ -20,6 +20,7 @@ import version from '../utils/version'; import { connect } from "react-redux"; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; import { PRIVACY_POLICY_URL, TERMS_OF_SERVICE_URL } from '../constants'; +import { walletReset } from '../actions'; const mapStateToProps = (state) => { return { @@ -28,6 +29,12 @@ const mapStateToProps = (state) => { }; }; +const mapDispatchToProps = dispatch => { + return { + walletReset: () => dispatch(walletReset()), + }; +}; + /** * Settings screen * @@ -79,7 +86,7 @@ class Settings extends React.Component { */ handleReset = () => { this.context.hideModal(); - wallet.resetWalletData(); + this.props.walletReset(); this.props.history.push('/welcome/'); } @@ -366,4 +373,4 @@ class Settings extends React.Component { } } -export default connect(mapStateToProps)(Settings); +export default connect(mapStateToProps, mapDispatchToProps)(Settings); diff --git a/src/screens/WalletVersionError.js b/src/screens/WalletVersionError.js index e648777a..5e9b8d8d 100644 --- a/src/screens/WalletVersionError.js +++ b/src/screens/WalletVersionError.js @@ -10,10 +10,8 @@ import { t } from 'ttag'; import logo from '../assets/images/hathor-white-logo.png'; import wallet from '../utils/wallet'; import Version from '../components/Version'; -import $ from 'jquery'; -import ModalBackupWords from '../components/ModalBackupWords'; import HathorAlert from '../components/HathorAlert'; -import { updateWords } from '../actions/index'; +import { updateWords, walletReset } from '../actions/index'; import { connect } from "react-redux"; import hathorLib from '@hathor/wallet-lib'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; @@ -22,6 +20,7 @@ import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; const mapDispatchToProps = dispatch => { return { updateWords: (data) => dispatch(updateWords(data)), + walletReset: () => dispatch(walletReset()), }; }; @@ -81,7 +80,7 @@ class WalletVersionError extends React.Component { */ handleReset = () => { this.context.hideModal(); - wallet.resetWalletData(); + this.props.walletReset(); this.props.history.push('/welcome/'); } diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 354b975e..8718ea5f 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -10,25 +10,22 @@ import { DEBUG_LOCAL_DATA_KEYS, WALLET_HISTORY_COUNT, METADATA_CONCURRENT_DOWNLOAD, + IGNORE_WS_TOGGLE_FLAG, } from '../constants'; -import { FeatureFlags } from '../featureFlags'; import store from '../store/index'; import { loadingAddresses, startWalletRequested, historyUpdate, - sharedAddressUpdate, reloadData, cleanData, updateTokenHistory, tokenMetadataUpdated, - partiallyUpdateHistoryAndBalance, resetSelectedTokenIfNeeded, } from '../actions/index'; import { constants as hathorConstants, errors as hathorErrors, - HathorWallet, wallet as oldWalletUtil, walletUtils, storage, @@ -451,19 +448,6 @@ const wallet = { store.dispatch(cleanData()); }, - /* - * Clean all data from everything - * - * @memberof Wallet - * @inner - */ - async resetWalletData() { - await FeatureFlags.clearIgnoreWalletServiceFlag(); - - this.cleanWalletRedux(); - oldWalletUtil.resetWalletData(); - }, - /* * Reload data in the localStorage * From 3391af46c92007887aae9792b13dd14f6ac3a354 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Thu, 13 Apr 2023 14:48:58 -0300 Subject: [PATCH 02/23] refactor: proposal token channel (#401) * refactor: proposal token channel * docs: jsdocs and variable naming * fix: reasonable concurrent threads * docs: improves jsdocs * style: improves variable naming and docs --- src/sagas/tokens.js | 57 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/sagas/tokens.js b/src/sagas/tokens.js index 78c165cc..46335800 100644 --- a/src/sagas/tokens.js +++ b/src/sagas/tokens.js @@ -21,7 +21,9 @@ import { tokenFetchBalanceFailed, tokenFetchHistoryRequested, tokenFetchHistorySuccess, - tokenFetchHistoryFailed, proposalTokenFetchSuccess, proposalTokenFetchFailed, + tokenFetchHistoryFailed, + proposalTokenFetchSuccess, + proposalTokenFetchFailed, } from '../actions'; import { t } from "ttag"; @@ -281,6 +283,51 @@ export function* monitorSelectedToken() { } } +/** + * This saga will create a channel to queue PROPOSAL_TOKEN_FETCH_REQUESTED actions and + * consumers that will run in parallel consuming those actions. + * + * This will mainly be used in the context of the Atomic Swap, retrieving names and symbols for all the tokens + * present in the listened proposals for this wallet. + * + * More information about channels can be read on https://redux-saga.js.org/docs/api/#takechannel + */ +function* fetchProposalTokenDataQueue() { + const fetchProposalTokenDataChannel = yield call(channel); + + // Fork CONCURRENT_FETCH_REQUESTS threads to download token data ( name and symbol ) + for (let i = 0; i < CONCURRENT_FETCH_REQUESTS; i += 1) { + yield fork(fetchProposalTokenDataConsumer, fetchProposalTokenDataChannel); + } + + while (true) { + const action = yield take(types.PROPOSAL_TOKEN_FETCH_REQUESTED); + yield put(fetchProposalTokenDataChannel, action); + } +} + +/** + * This saga will consume the fetchProposalTokenDataChannel for PROPOSAL_TOKEN_FETCH_REQUESTED actions + * and wait until the PROPOSAL_TOKEN_FETCH_SUCCESS action is dispatched with the specific proposalId + */ +function* fetchProposalTokenDataConsumer(fetchProposalTokenDataChannel) { + while (true) { + const action = yield take(fetchProposalTokenDataChannel); + + yield fork(fetchProposalTokenData, action); + + // Wait until the success action is dispatched before consuming another action + yield take( + specificTypeAndPayload([ + types.PROPOSAL_TOKEN_FETCH_SUCCESS, + types.PROPOSAL_TOKEN_FETCH_FAILED, + ], { + tokenId: action.proposalId, + }), + ); + } +} + /** * * @param {string} action.tokenUid Token identifier to fetch data from @@ -306,12 +353,12 @@ function* fetchProposalTokenData(action) { const wallet = yield select((state) => state.wallet); - // Fetching from the fullnode + // Fetching name and symbol data from the fullnode const updatedTokenDetails = yield wallet.getTokenDetails(tokenUid); yield put(proposalTokenFetchSuccess(tokenUid, updatedTokenDetails.tokenInfo)); } catch (e){ - console.error(`Error downloading metadata of proposal token`, tokenUid, e.message); - yield put(proposalTokenFetchFailed(tokenUid, t`An error ocurred while fetching this token`)); + console.error(`Error downloading proposal token data`, tokenUid, e.message); + yield put(proposalTokenFetchFailed(tokenUid, t`An error occurred while fetching this token data`)); } } @@ -320,8 +367,8 @@ export function* saga() { fork(fetchTokenMetadataQueue), fork(fetchTokenBalanceQueue), fork(monitorSelectedToken), + fork(fetchProposalTokenDataQueue), takeEvery(types.TOKEN_FETCH_HISTORY_REQUESTED, fetchTokenHistory), takeEvery('new_tokens', routeTokenChange), - takeEvery(types.PROPOSAL_TOKEN_FETCH_REQUESTED, fetchProposalTokenData) ]); } From 0184ac6d5171b3de2c1557b3bc9aedc926541706 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Tue, 9 May 2023 00:08:35 -0300 Subject: [PATCH 03/23] fix: exhibition of swap with zero balance (#402) * fix: exhibition of swap with zero balance * refactor: simplify sendingBalances --- src/__tests__/utils/atomicSwap.test.js | 306 ++++++++++++++++++ .../atomic-swap/ProposalBalanceTable.js | 9 +- src/utils/atomicSwap.js | 4 +- 3 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/utils/atomicSwap.test.js diff --git a/src/__tests__/utils/atomicSwap.test.js b/src/__tests__/utils/atomicSwap.test.js new file mode 100644 index 00000000..061e5d5c --- /dev/null +++ b/src/__tests__/utils/atomicSwap.test.js @@ -0,0 +1,306 @@ +import { calculateExhibitionData, PROPOSAL_DOWNLOAD_STATUS } from "../../utils/atomicSwap"; +import { PartialTxProposal } from "@hathor/wallet-lib"; + +const customTokenUid = '00003b47ce1a6774cfc132169122c38c15fbc4a7f43487cf1041ff4826c1842e'; +/** + * Mocked wallet to help with the tests + * @type {HathorWallet} + */ +const wallet = { + getNetworkObject: () => ({ name: 'privatenet' }), + isAddressMine: (address) => { + return address.startsWith('mine-'); + }, +}; + +const mockNetwork = { + name: "privatenet", +}; + +function createNewProposal() { + const np = new PartialTxProposal(mockNetwork); + + // Mock another wallet sending 200 HTR and receiving 1 of a custom token + np.partialTx.inputs = [ + { + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 6400, + authorities: 0, + token: "00", + address: "other-1" + } + ] + np.partialTx.outputs = [ + { + value: 6200, + tokenData: 0, + decodedScript: { + address: { base58: "other-2" }, + timelock: null + }, + token: "00", + isChange: true, + authorities: 0 + }, + { + value: 1, + tokenData: 1, + decodedScript: { + address: { base58: "other-2" }, + timelock: null + }, + token: customTokenUid, + isChange: false, + authorities: 0 + } + ] + + return np; +} + +describe('calculateExhibitionData', () => { + const deserializeSpy = jest.spyOn(PartialTxProposal, 'fromPartialTx'); + const fakePartialTx = { serialize: () => 'fakeSerializedPartialTx' }; + + it('should return an empty array when there is no interaction with the wallet', () => { + deserializeSpy.mockImplementationOnce(() => createNewProposal()) + const cachedTokens = {}; + const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + expect(results).toStrictEqual([]); + }) + + it('should return the correct balance for a single receive', () => { + // Mock receiving 200 HTR + deserializeSpy.mockImplementationOnce(() => { + const np = createNewProposal(); + np.partialTx.outputs.push({ + value: 200, + tokenData: 0, + decodedScript: { + address: { base58: "mine-1" }, + timelock: null + }, + token: "00", + isChange: false, + authorities: 0, + isAuthority: () => false, + }) + return np; + }) + const cachedTokens = {}; + const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + expect(results).toStrictEqual([ + expect.objectContaining({ + tokenUid: '00', + receiving: 200, + }) + ]); + }) + + it('should return the correct balance for a single send', () => { + // Mock sending 1 custom token + deserializeSpy.mockImplementationOnce(() => { + const np = createNewProposal(); + np.partialTx.inputs.push({ + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 1, + authorities: 0, + token: customTokenUid, + address: "mine-1", + isAuthority: () => false, + }) + return np; + }) + const cachedTokens = {}; + const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + expect(results).toStrictEqual([ + expect.objectContaining({ + tokenUid: customTokenUid, + sending: 1, + }) + ]); + }) + + it('should return the correct balance for sending and receiving multiple tokens', () => { + // Mock sending 1 custom token + deserializeSpy.mockImplementationOnce(() => { + const np = createNewProposal(); + np.partialTx.inputs.push({ + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 1, + authorities: 0, + token: customTokenUid, + address: "mine-1", + isAuthority: () => false, + }) + np.partialTx.outputs.push({ + value: 200, + tokenData: 0, + decodedScript: { + address: { base58: "mine-1" }, + timelock: null + }, + token: "00", + isChange: false, + authorities: 0, + isAuthority: () => false, + }) + return np; + }) + const cachedTokens = {}; + const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + expect(results).toStrictEqual(expect.arrayContaining([ + expect.objectContaining({ + tokenUid: customTokenUid, + sending: 1, + }), + expect.objectContaining({ + tokenUid: '00', + receiving: 200, + }) + ])); + }) + + it('should return the correct balance for sending and receiving zero tokens', () => { + // Mock sending 1 custom token + deserializeSpy.mockImplementationOnce(() => { + const np = createNewProposal(); + np.partialTx.inputs.push({ + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 1, + authorities: 0, + token: '00', + address: "mine-1", + isAuthority: () => false, + }) + np.partialTx.outputs.push({ + value: 1, + tokenData: 0, + decodedScript: { + address: { base58: "mine-2" }, + timelock: null + }, + token: "00", + isChange: false, + authorities: 0, + isAuthority: () => false, + }) + return np; + }) + const cachedTokens = {}; + const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + expect(results).toStrictEqual([ + expect.objectContaining({ + tokenUid: '00', + }) + ]); + }) + + it('should return the correct balance for all conditions above simultaneously', () => { + deserializeSpy.mockImplementationOnce(() => { + const np = createNewProposal(); + // Token '00' has zero balance + np.partialTx.inputs.push({ + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 1, + authorities: 0, + token: '00', + address: "mine-1", + isAuthority: () => false, + }) + np.partialTx.outputs.push({ + value: 1, + tokenData: 0, + decodedScript: { + address: { base58: "mine-2" }, + timelock: null + }, + token: "00", + isChange: false, + authorities: 0, + isAuthority: () => false, + }) + + // Token 'fake1' has sending balance + np.partialTx.inputs.push({ + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 2, + authorities: 0, + token: 'fake1', + address: "mine-3", + isAuthority: () => false, + }) + + // Token 'fake2' has receiving balance + np.partialTx.outputs.push({ + value: 3, + tokenData: 0, + decodedScript: { + address: { base58: "mine-4" }, + timelock: null + }, + token: "fake2", + isChange: false, + authorities: 0, + isAuthority: () => false, + }) + + // Is not participating in token 'fake3' + np.partialTx.inputs.push({ + hash: "000000e5924f0b07a626fd47839f85983a0faf14a337ac85e53cc6bb877bd14a", + index: 0, + data: null, + value: 4, + authorities: 0, + token: 'fake3', + address: "other-1", + isAuthority: () => false, + }) + np.partialTx.outputs.push({ + value: 3, + tokenData: 0, + decodedScript: { + address: { base58: "other-2" }, + timelock: null + }, + token: "fake3", + isChange: false, + authorities: 0, + isAuthority: () => false, + }) + + return np; + }) + const cachedTokens = {}; + const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + expect(results).toStrictEqual([ + expect.objectContaining({ + tokenUid: '00', + }), + expect.objectContaining({ + tokenUid: 'fake1', + sending: 2 + }), + expect.objectContaining({ + tokenUid: 'fake2', + receiving: 3 + }), + ]); + expect(results).not.toContain( + expect.objectContaining({ + tokenUid: 'fake3', + }),) + }) +}) diff --git a/src/components/atomic-swap/ProposalBalanceTable.js b/src/components/atomic-swap/ProposalBalanceTable.js index 27066bc8..fb3ba35c 100644 --- a/src/components/atomic-swap/ProposalBalanceTable.js +++ b/src/components/atomic-swap/ProposalBalanceTable.js @@ -15,12 +15,17 @@ import { t } from "ttag"; * @param {DisplayBalance[]} balance */ export function ProposalBalanceTable ({ partialTx, wallet, balance }) { - const sendingBalances = balance.filter(b => b.sending > 0); const receivingBalances = balance.filter(b => b.receiving > 0); + // If the wallet participates in the proposal with a token with zero balance, it is displayed as "sending" + const sendingBalances = balance.filter(b => { + return b.sending >= 0; + }); const renderRows = () => { const renderOne = (amount, symbol) => { - if (!amount || !symbol) return ''; + if (!amount && !symbol) { + return ''; + } return {helpers.renderValue(amount, false)} {symbol} } diff --git a/src/utils/atomicSwap.js b/src/utils/atomicSwap.js index 8282f689..897fd239 100644 --- a/src/utils/atomicSwap.js +++ b/src/utils/atomicSwap.js @@ -140,14 +140,14 @@ export function calculateExhibitionData(partialTx, cachedTokens, wallet) { tokenUid, symbol: '', name: '', - status: PROPOSAL_DOWNLOAD_STATUS.LOADING + status: PROPOSAL_DOWNLOAD_STATUS.LOADING, }; } return cachedTokens[tokenUid]; } - const txProposal = new PartialTxProposal.fromPartialTx(partialTx.serialize(), wallet.getNetworkObject()); + const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), wallet.getNetworkObject()); const balance = txProposal.calculateBalance(wallet); // Calculating the difference between them both From 2f48de9775379130882d7240ea6d57e97682c679 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Wed, 17 May 2023 17:17:44 -0300 Subject: [PATCH 04/23] feat: Atomic Swap Create with backend (#400) * feat: create proposals on backend * feat: get proposals * feat: fetching proposals on wallet load * style: fix typo * feat: proposal enrichment on saga * style: fixes indentation * feat: error message handling * fix: loader and jsdocs * fix: package-lock.json * docs: minor adjustments * fix: invalid ttag calls * fix: removes unused variables * fix: redux state updating on new swap * fix: converts amount of tokens to string * fix: converts amount of tokens to string * fix: minor adjustments * docs: improves comments * refactor: pAmountTokens is now a Number * refactor: cleanup scope reduced * docs: fixes to comments and jsdocs * refactor: single saga for creating proposals * fix: failure data stored in mock proposal * fix: code cleanup * feat: err handling through lastFailedRequest obj * docs: code cleanup * fix: clean the error message on click * docs: adds missing doc * refactor: populates redux data on create * fix: updates persistent storage on create * refactor: new proposal data moved to utils * docs: fixes docstring --- package-lock.json | 6 +- package.json | 2 +- src/actions/index.js | 33 +++++-- src/reducers/index.js | 67 +++++++------ src/sagas/atomicSwap.js | 101 +++++++++++++++++-- src/sagas/wallet.js | 18 ++++ src/screens/atomic-swap/ImportExisting.js | 12 ++- src/screens/atomic-swap/NewSwap.js | 35 +++---- src/screens/atomic-swap/ProposalList.js | 13 ++- src/utils/atomicSwap.js | 114 +++++++++++++++++----- 10 files changed, 303 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index 464040f1..bc758e83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1600,9 +1600,9 @@ } }, "@hathor/wallet-lib": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-0.44.2.tgz", - "integrity": "sha512-7psMvWWUf5JLgyGImAFUDtVuQJEZnDngw9SUhoh5WR/ZM3eOiovmFZ8lO417ObF8cpAJ7sZugWvvAt9t0k94mA==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-0.46.1.tgz", + "integrity": "sha512-msLP8Xuqv+IRjcBYeSRs5MO77kJmqJyMTAVfP692JCgGcMzYKV6nsiL61yzmdfaL+sUJAPsYFEJqpEzZvdrdCA==", "requires": { "axios": "^0.21.4", "bitcore-lib": "^8.25.10", diff --git a/package.json b/package.json index c98155bf..e04075bc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "version": "0.26.0", "private": true, "dependencies": { - "@hathor/wallet-lib": "^0.44.2", + "@hathor/wallet-lib": "^0.46.1", "@ledgerhq/hw-transport-node-hid": "^6.27.1", "@sentry/electron": "^3.0.7", "babel-polyfill": "^6.26.0", diff --git a/src/actions/index.js b/src/actions/index.js index 5530a91d..4f6193d6 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -22,13 +22,15 @@ export const types = { TOKEN_FETCH_HISTORY_SUCCESS: 'TOKEN_FETCH_HISTORY_SUCCESS', TOKEN_FETCH_HISTORY_FAILED: 'TOKEN_FETCH_HISTORY_FAILED', SET_ENABLE_ATOMIC_SWAP: 'SET_ENABLE_ATOMIC_SWAP', + PROPOSAL_LIST_UPDATED: 'PROPOSAL_LIST_UPDATED', PROPOSAL_FETCH_REQUESTED: 'PROPOSAL_FETCH_REQUESTED', PROPOSAL_FETCH_SUCCESS: 'PROPOSAL_FETCH_SUCCESS', PROPOSAL_FETCH_FAILED: 'PROPOSAL_FETCH_FAILED', + PROPOSAL_UPDATED: 'PROPOSAL_UPDATED', PROPOSAL_TOKEN_FETCH_REQUESTED: 'PROPOSAL_TOKEN_FETCH_REQUESTED', PROPOSAL_TOKEN_FETCH_SUCCESS: 'PROPOSAL_TOKEN_FETCH_SUCCESS', PROPOSAL_TOKEN_FETCH_FAILED: 'PROPOSAL_TOKEN_FETCH_FAILED', - PROPOSAL_GENERATED: 'PROPOSAL_GENERATED', + PROPOSAL_CREATE_REQUESTED: 'PROPOSAL_CREATE_REQUESTED', PROPOSAL_REMOVED: 'PROPOSAL_REMOVED', PROPOSAL_IMPORTED: 'PROPOSAL_IMPORTED', TOKEN_INVALIDATE_HISTORY: 'TOKEN_INVALIDATE_HISTORY', @@ -282,6 +284,15 @@ export const tokenFetchBalanceFailed = (tokenId) => ({ */ export const setEnableAtomicSwap = (useAtomicSwap) => ({ type: types.SET_ENABLE_ATOMIC_SWAP, payload: useAtomicSwap }); +/** + * @param {Record} listenedProposalsMap + * A map of listened proposals + */ +export const proposalListUpdated = (listenedProposalsMap) => ({ + type: types.PROPOSAL_LIST_UPDATED, + listenedProposalsMap +}); + /** * @param {string} proposalId The proposalId to request data * @param {string} password The proposal's password to decode its data @@ -314,6 +325,16 @@ export const proposalFetchFailed = (proposalId, errorMessage) => ({ errorMessage, }); +/** + * @param {string} proposalId The proposalId to store data + * @param {unknown} data The updated proposal data + */ +export const proposalUpdated = (proposalId, data) => ({ + type: types.PROPOSAL_UPDATED, + proposalId, + data, +}); + /** * @param {string} tokenUid The token identifier to fetch */ @@ -343,15 +364,13 @@ export const proposalTokenFetchFailed = (tokenUid, errorMessage) => ({ }); /** - * @param {string} proposalId + * @param {string} partialTx * @param {string} password - * @param {ProposalData} data The generated proposal object */ -export const proposalGenerated = (proposalId, password, data) => ({ - type: types.PROPOSAL_GENERATED, - proposalId, +export const proposalCreateRequested = (partialTx, password) => ({ + type: types.PROPOSAL_CREATE_REQUESTED, password, - data, + partialTx, }); /** diff --git a/src/reducers/index.js b/src/reducers/index.js index a9c32b22..0243d15f 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -6,14 +6,13 @@ */ import hathorLib from '@hathor/wallet-lib'; -import { VERSION } from '../constants'; +import { FEATURE_TOGGLE_DEFAULTS, VERSION } from '../constants'; import { types } from '../actions'; import { get } from 'lodash'; import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; import { WALLET_STATUS } from '../sagas/wallet'; import { PROPOSAL_DOWNLOAD_STATUS } from '../utils/atomicSwap'; import { HATHOR_TOKEN_CONFIG } from "@hathor/wallet-lib/lib/constants"; -import { FEATURE_TOGGLE_DEFAULTS } from '../constants'; /** * @typedef TokenHistory @@ -222,20 +221,22 @@ const rootReducer = (state = initialState, action) => { return onTokenFetchHistoryFailed(state, action); case types.SET_ENABLE_ATOMIC_SWAP: return onSetUseAtomicSwap(state, action); + case types.PROPOSAL_LIST_UPDATED: + return onProposalListUpdated(state, action); case types.PROPOSAL_FETCH_REQUESTED: return onProposalFetchRequested(state, action); case types.PROPOSAL_FETCH_SUCCESS: return onProposalFetchSuccess(state, action); case types.PROPOSAL_FETCH_FAILED: return onProposalFetchFailed(state, action); + case types.PROPOSAL_UPDATED: + return onProposalUpdate(state, action); case types.PROPOSAL_TOKEN_FETCH_REQUESTED: return onProposalTokenFetchRequested(state, action); case types.PROPOSAL_TOKEN_FETCH_SUCCESS: return onProposalTokenFetchSuccess(state, action); case types.PROPOSAL_TOKEN_FETCH_FAILED: return onProposalTokenFetchFailed(state, action); - case types.PROPOSAL_GENERATED: - return onNewProposalGeneration(state, action); case types.PROPOSAL_REMOVED: return onProposalRemoved(state, action); case types.PROPOSAL_IMPORTED: @@ -688,6 +689,17 @@ export const onSetUseAtomicSwap = (state, action) => { }; }; +/** + * @param {Record} action.listenedProposalsMap +* A map of listened proposals + */ +export const onProposalListUpdated = (state, action) => { + return { + ...state, + proposals: action.listenedProposalsMap + } +} + /** * @param {String} action.proposalId The proposal id to fetch from the backend */ @@ -756,6 +768,28 @@ export const onProposalFetchFailed = (state, action) => { }; }; +/** + * @param {String} action.proposalId - The proposalId to mark as success + * @param {Object} action.data - The proposal history information to store on redux + */ +export const onProposalUpdate = (state, action) => { + const { proposalId, data } = action; + + const oldState = get(state.proposals, proposalId, { id: proposalId }) + + return { + ...state, + proposals: { + ...state.proposals, + [proposalId]: { + ...oldState, + updatedAt: new Date().getTime(), + data, + }, + }, + }; +}; + /** * @param {String} action.tokenUid The token identifier to fetch */ @@ -827,33 +861,10 @@ export const onProposalTokenFetchFailed = (state, action) => { /** * @param {String} action.proposalId - The new proposalId to store - * @param {String} action.password - The proposal password - * @param {Object} action.data - The proposal history information to store on redux - */ -export const onNewProposalGeneration = (state, action) => { - const { proposalId, password, data } = action; - - return { - ...state, - proposals: { - ...state.proposals, - [proposalId]: { - id: proposalId, - password, - status: PROPOSAL_DOWNLOAD_STATUS.READY, - updatedAt: new Date().getTime(), - data - }, - }, - }; -}; - -/** - * @param {String} action.proposalId - The new proposalId to store + * @param {String} action.password - The proposal's password */ export const onProposalImported = (state, action) => { const { proposalId, password } = action; - return { ...state, proposals: { diff --git a/src/sagas/atomicSwap.js b/src/sagas/atomicSwap.js index 0fb3f0c4..d8945ec5 100644 --- a/src/sagas/atomicSwap.js +++ b/src/sagas/atomicSwap.js @@ -5,15 +5,26 @@ * LICENSE file in the root directory of this source tree. */ -import { all, call, fork, put, select, take, } from 'redux-saga/effects'; +import { all, call, fork, put, select, take, takeEvery, } from 'redux-saga/effects'; import { channel } from "redux-saga"; -import { proposalFetchFailed, proposalFetchSuccess, types } from "../actions"; +import { + importProposal, + lastFailedRequest, + proposalFetchFailed, + proposalFetchSuccess, + proposalUpdated, + types +} from "../actions"; import { specificTypeAndPayload } from "./helpers"; import { get } from 'lodash'; import { + ATOMIC_SWAP_SERVICE_ERRORS, + generateReduxObjFromProposal, + updatePersistentStorage, PROPOSAL_DOWNLOAD_STATUS, } from "../utils/atomicSwap"; import { t } from "ttag"; +import { swapService } from '@hathor/wallet-lib' const CONCURRENT_FETCH_REQUESTS = 5; @@ -76,14 +87,87 @@ function* fetchProposalData(action) { return; } - // TODO: Implement the actual communication with the backend - throw new Error(`Proposal fetching not implemented.`) + // Fetch data from the backend + const responseData = yield swapService.get(proposalId, password); + yield put(proposalFetchSuccess(proposalId, responseData)); + + // On success, build the proposal object locally and enrich it + const wallet = yield select((state) => state.wallet); + const newData = generateReduxObjFromProposal( + proposalId, + password, + responseData.partialTx, + wallet, + ); + + // Adding the newly generated metadata to the proposal + const enrichedData = { ...responseData, ...newData.data }; + yield put(proposalUpdated(proposalId, enrichedData)); + } catch (e) { + let errorMessage; + const backendErrorData = e.response?.data || {}; + switch (backendErrorData.code) { + case ATOMIC_SWAP_SERVICE_ERRORS.ProposalNotFound: + errorMessage = t`Proposal not found.`; + break; + case ATOMIC_SWAP_SERVICE_ERRORS.IncorrectPassword: + errorMessage = t`Incorrect password.`; + break; + default: + errorMessage = t`An error occurred while fetching this proposal.`; + } + yield put(proposalFetchFailed(proposalId, errorMessage)); + } +} + +/** + * Makes the request to the backend to create a proposal and returns its results via saga events + * @param {string} action.partialTx + * @param {string} action.password + */ +function* createProposalOnBackend(action) { + const { password, partialTx } = action; + + try { + // Cleaning up the error handling redux object + yield put(lastFailedRequest(undefined)) + + // Request an identifier from the service backend + const { success, id: proposalId } = yield swapService.create(partialTx, password); + + // Error handling + if (!success) { + yield put(lastFailedRequest({ + message: t`An error occurred while creating this proposal.` + })) + return; + } + + // Generate a minimal redux object on the application state + yield(put(importProposal(proposalId, password))); + + // Enrich the PartialTx with exhibition metadata + const wallet = yield select((state) => state.wallet); + const newProposalReduxObj = generateReduxObjFromProposal( + proposalId, + password, + partialTx, + wallet, + { newProposal: true } + ); + + // Insert generated data into state as a fetch saga results + yield put(proposalFetchSuccess(proposalId, newProposalReduxObj.data)); + + // Update the persistent storage with the new addition + const allProposals = yield select((state) => state.proposals); + updatePersistentStorage(allProposals); - // yield put(proposalFetchFailed(proposalId, "Proposal not found")); - // yield put(proposalFetchFailed(proposalId, "Incorrect password")); - // yield put(proposalFetchSuccess(proposalId, apiResponseData)); + // Navigating to the Edit Swap screen with this proposal + const routerHistory = yield select((state) => state.routerHistory); + routerHistory.replace(`/wallet/atomic_swap/proposal/${proposalId}`); } catch (e) { - yield put(proposalFetchFailed(proposalId, t`An error occurred while fetching this proposal.`)); + yield put(lastFailedRequest({ message: e.message })); } } @@ -92,5 +176,6 @@ function* fetchProposalData(action) { export function* saga() { yield all([ fork(fetchProposalDataQueue), + takeEvery(types.PROPOSAL_CREATE_REQUESTED, createProposalOnBackend) ]); } diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index ddd62691..8eae7877 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -7,6 +7,7 @@ import { tokens as tokensUtils, constants as hathorLibConstants, config, + storage, } from '@hathor/wallet-lib'; import { takeLatest, @@ -55,6 +56,8 @@ import { sharedAddressUpdate, walletRefreshSharedAddress, setEnableAtomicSwap, + proposalListUpdated, + proposalFetchRequested, walletResetSuccess, reloadWalletRequested, } from '../actions'; @@ -65,6 +68,8 @@ import { } from './helpers'; import { fetchTokenData } from './tokens'; import walletUtils from '../utils/wallet'; +import { initializeSwapServiceBaseUrlForWallet } from "../utils/atomicSwap"; +import walletUtil from "../utils/wallet"; export const WALLET_STATUS = { READY: 'ready', @@ -247,6 +252,19 @@ 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) + // Initialize listened proposals list + const listenedProposals = walletUtil.getListenedProposals(); + yield put(proposalListUpdated(listenedProposals)); + + // Fetch all proposals from service backend + for (const [pId, p] of Object.entries(listenedProposals)) { + yield put(proposalFetchRequested(pId, p.password)); + } + } + // Wallet start called, we need to show the loading addresses screen routerHistory.replace('/loading_addresses'); diff --git a/src/screens/atomic-swap/ImportExisting.js b/src/screens/atomic-swap/ImportExisting.js index 3526437f..16d3053e 100644 --- a/src/screens/atomic-swap/ImportExisting.js +++ b/src/screens/atomic-swap/ImportExisting.js @@ -11,7 +11,10 @@ import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { importProposal, proposalFetchRequested } from "../../actions"; -import { PROPOSAL_DOWNLOAD_STATUS } from "../../utils/atomicSwap"; +import { + PROPOSAL_DOWNLOAD_STATUS, + updatePersistentStorage +} from "../../utils/atomicSwap"; export default function ImportExisting(props) { // Internal state @@ -55,15 +58,16 @@ export default function ImportExisting(props) { return; } + // Error handling if (existingProposal.status === PROPOSAL_DOWNLOAD_STATUS.FAILED) { setErrorMessage(existingProposal.errorMessage); setIsLoading(false); return; } - if (existingProposal) { - navigateToProposal(proposalId); - } + // The proposal was successfully imported: updating persistent storage and navigating to it + updatePersistentStorage(allProposals); + navigateToProposal(proposalId); }) return
diff --git a/src/screens/atomic-swap/NewSwap.js b/src/screens/atomic-swap/NewSwap.js index 16c51344..096413b5 100644 --- a/src/screens/atomic-swap/NewSwap.js +++ b/src/screens/atomic-swap/NewSwap.js @@ -6,47 +6,44 @@ */ import BackButton from "../../components/BackButton"; import React, { useEffect, useState } from "react"; -import { useHistory } from 'react-router-dom'; import { t } from "ttag"; import Loading from "../../components/Loading"; -import { generateEmptyProposalFromPassword, updatePersistentStorage } from "../../utils/atomicSwap"; +import { + generateEmptyProposal, +} from "../../utils/atomicSwap"; import { useDispatch, useSelector } from "react-redux"; -import { proposalGenerated } from "../../actions"; +import { proposalCreateRequested } from "../../actions"; export default function NewSwap (props) { - const [proposalId, setProposalId] = useState(''); const [password, setPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); - const allProposals = useSelector(state => state.proposals); const wallet = useSelector(state => state.wallet); - const history = useHistory(); + + // Global interactions + const lastFailedRequest = useSelector(state => state.lastFailedRequest); const dispatch = useDispatch(); const [errorMessage, setErrorMessage] = useState(''); - const navigateToProposal = (pId) => { - history.replace(`/wallet/atomic_swap/proposal/${pId}`); - } - const createClickHandler = () => { if (password.length < 3) { - setErrorMessage('Please insert a password more than 3 characters long'); + setErrorMessage(t`Please insert a password more than 3 characters long`); return; } + setErrorMessage(''); setIsLoading(true); - const { data, id } = generateEmptyProposalFromPassword(password, wallet); - setProposalId(id); - dispatch(proposalGenerated(id, password, data)) + const newPartialTx = generateEmptyProposal(wallet); + dispatch(proposalCreateRequested(newPartialTx, password)); } useEffect(() => { - // If this proposal exists, navigate to it immediately - if (allProposals[proposalId]) { - updatePersistentStorage(allProposals); - navigateToProposal(proposalId); + // Shows the error message if it happens + if (lastFailedRequest && lastFailedRequest.message) { + setErrorMessage(lastFailedRequest.message); + setIsLoading(false); } - }) + }, [lastFailedRequest]); return
diff --git a/src/screens/atomic-swap/ProposalList.js b/src/screens/atomic-swap/ProposalList.js index 09a08872..999b1210 100644 --- a/src/screens/atomic-swap/ProposalList.js +++ b/src/screens/atomic-swap/ProposalList.js @@ -12,7 +12,10 @@ import { useDispatch, useSelector } from "react-redux"; import { useHistory } from 'react-router-dom'; import Loading from "../../components/Loading"; import { proposalFetchRequested, proposalRemoved } from "../../actions"; -import { PROPOSAL_DOWNLOAD_STATUS, updatePersistentStorage } from "../../utils/atomicSwap"; +import { + PROPOSAL_DOWNLOAD_STATUS, + updatePersistentStorage +} from "../../utils/atomicSwap"; import walletUtil from "../../utils/wallet"; import { GlobalModalContext, MODAL_TYPES } from '../../components/GlobalModal'; @@ -63,7 +66,9 @@ export default function ProposalList (props) { for (const [proposalId, proposal] of Object.entries(proposals)) { const pId = proposalId; const password = proposal.password; - const pAmountTokens = proposal.data?.amountTokens; // Get this from proposal + const pAmountTokens = proposal.data + ? proposal.data.amountTokens || 0 + : undefined; const pStatus = proposal.data?.signatureStatus; const isLoading = proposal.status === PROPOSAL_DOWNLOAD_STATUS.LOADING || proposal.status === PROPOSAL_DOWNLOAD_STATUS.INVALIDATED; const isLoaded = proposal.status === PROPOSAL_DOWNLOAD_STATUS.READY; @@ -99,9 +104,7 @@ export default function ProposalList (props) { { isLoaded && {pAmountTokens} } { isLoaded && {pStatus} } - removeProposalClickHandler(e, pId)}> ) diff --git a/src/utils/atomicSwap.js b/src/utils/atomicSwap.js index 897fd239..8ac4d06b 100644 --- a/src/utils/atomicSwap.js +++ b/src/utils/atomicSwap.js @@ -5,13 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import { v4 } from 'uuid' -import hathorLib, { PartialTx, PartialTxInputData, PartialTxProposal } from "@hathor/wallet-lib"; -import { DECIMAL_PLACES, TOKEN_MINT_MASK, TOKEN_MELT_MASK, HATHOR_TOKEN_CONFIG } from "@hathor/wallet-lib/lib/constants"; +import hathorLib, { PartialTx, PartialTxInputData, PartialTxProposal, storage } from "@hathor/wallet-lib"; +import { HATHOR_TOKEN_CONFIG, TOKEN_MELT_MASK, TOKEN_MINT_MASK } from "@hathor/wallet-lib/lib/constants"; import { get } from 'lodash'; import walletUtil from "./wallet"; -const { wallet: oldWallet } = hathorLib; +const { wallet: oldWallet, config: hathorLibConfig } = hathorLib; /** * @typedef ProposalData * @property {string} id Proposal identifier @@ -59,29 +58,23 @@ export const PROPOSAL_SIGNATURE_STATUS = { SENT: 'Sent', } +export const ATOMIC_SWAP_SERVICE_ERRORS = { + ProposalNotFound: 'PROPOSAL_NOT_FOUND', + DuplicateProposalId: 'DUPLICATE_PROPOSAL_ID', + InvalidPassword: 'INVALID_PASSWORD', + IncorrectPassword: 'INCORRECT_PASSWORD', + VersionConflict: 'VERSION_CONFLICT', + UnknownError: 'UNKNOWN_ERROR', +} + /** - * Generates an empty proposal for storing on redux - * @param {string} password + * Generates the serialized string of an empty proposal for the current wallet * @param {HathorWallet} wallet Current wallet in use - * @return {{id:string, password:string, data:ProposalData}} + * @return {string} */ -export function generateEmptyProposalFromPassword(password, wallet) { +export function generateEmptyProposal(wallet) { const partialTx = new PartialTx(wallet.getNetworkObject()); - const pId = v4(); - - return { - id: pId, - password, - data: { - id: pId, - partialTx: partialTx.serialize(), - signatures: null, - amountTokens: 0, - signatureStatus: PROPOSAL_SIGNATURE_STATUS.OPEN, - timestamp: undefined, - history: [] - }, - }; + return partialTx.serialize(); } /** @@ -340,3 +333,78 @@ export const updatePersistentStorage = (proposalList) => { walletUtil.setListenedProposals(simplifiedStorage); } + +/** + * Calculates the object that will be stored in Redux, containing all the helper values for a more + * enriched exhibition on the Atomic Swap screens + * @param {string} proposalId + * @param {string} password + * @param {string} partialTx + * @param {HathorWallet} wallet + * @param [options] + * @param {string} [options.signatures] Optional signatures data for the proposal + * @param {boolean} [options.newProposal=false] Requests new proposal properties to be added + * @return {ReduxProposalData} + */ +export const generateReduxObjFromProposal = (proposalId, password, partialTx, wallet, options = {}) => { + /** @type ReduxProposalData */ + const rObj = { + id: proposalId, + password: password, + status: PROPOSAL_DOWNLOAD_STATUS.INVALIDATED, // We do not know the current state from the calculations + data: { // Only the minimum information. Any other data already retrieved should be added afterward + id: proposalId, + partialTx, + history: [], + }, + updatedAt: new Date().valueOf(), + }; + // Building the object to retrieve data from + const txProposal = new PartialTxProposal.fromPartialTx(partialTx, wallet.getNetworkObject()); + + // Calculating the amount of tokens + const balance = txProposal.calculateBalance(wallet); + rObj.data.amountTokens = Object.keys(balance).length; + + // Calculating signature status + let sigStatus; + const amountInputs = txProposal.partialTx.getTx().inputs.length; + const amountSigs = options.signatures + ? options.signatures.split('|').length - 2 // Removing the prefix and txHex, sigs remain + : 0; + if (amountSigs < 1) { + sigStatus = PROPOSAL_SIGNATURE_STATUS.OPEN; + } else if (amountSigs < amountInputs) { + sigStatus = PROPOSAL_SIGNATURE_STATUS.PARTIALLY_SIGNED; + } else { + sigStatus = PROPOSAL_SIGNATURE_STATUS.SIGNED; + } + rObj.data.signatureStatus = sigStatus; + + // Adding new proposal data, if requested + if (options.newProposal) { + rObj.data.version = 0; + rObj.data.timestamp = rObj.updatedAt; + } + + return rObj; +} + +/** + * Returns the Atomic Swap Service base URL, + * @param {string} network Network name for fetching the default base server url + * @returns {void} + */ +export function initializeSwapServiceBaseUrlForWallet(network) { + // XXX: This storage item is currently unchangeable via the wallet UI, and is available + // only for debugging purposes on networks other than mainnet and testnet + const configUrl = storage.getItem('wallet:atomic_swap_service:base_server') + // Configures Atomic Swap Service url. Prefers explicit config input, then network-based + if (configUrl) { + hathorLibConfig.setSwapServiceBaseUrl(configUrl); + } else { + hathorLibConfig.setSwapServiceBaseUrl( + hathorLibConfig.getSwapServiceBaseUrl(network) + ); + } +} From ecd64a5d4a8fd93220fa249070e96f75444d7a2d Mon Sep 17 00:00:00 2001 From: Luis Helder Date: Tue, 1 Aug 2023 18:11:39 -0300 Subject: [PATCH 05/23] docs: release signature guides (#405) * docs: remove warning about Windows that is not valid anymore * docs: add release signatures guide * fix: include package-lock.json in the check_version script --- README.md | 14 +++--- RELEASING.md | 112 +++++++++++++++++++++++++++++++++++++++++- scripts/check_version | 7 +++ 3 files changed, 124 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5ba27adb..c072f47b 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,6 @@ The wallet is developed using Javascript with [React](https://reactjs.org/). We You can download the newest version of the wallet for each specific platform from the [Releases page](https://github.com/HathorNetwork/hathor-wallet/releases). -### Warning Message for Windows: - -We are finishing the process of acquiring the certificates for Windows. While we don't get it you may see a warning message when opening the wallet. - -![Warning Windows](https://drive.google.com/thumbnail?id=1B5kLAXUMj4wmrRfmVtiQyoNe6Q7r8s_h&sz=w500-h375) - -This screen will show a warning, so you need to click on 'More info'. Another screen will appear, then just click the button 'Run anyway' to start the wallet. - ## Screenshots The basic view of the wallet. Note that different types of tokens are made possible in the Hathor Network. On the left hand side we see both a HTR tab and a MTK tab, for the Hathor token, and a different, ERC-20 like, token. @@ -173,6 +165,12 @@ Run `msgmerge pt-br/texts.po texts.pot -o pt-br/texts.po` to merge a pot file wi Finally, run `make i18n` to compile all po files to json files. You can use `make check_po` to check for problems in translations. +## Release + +There is a release guide in [RELEASE.md](/RELEASE.md). + +We ship GPG signatures for all release packages. Check our guide in [RELEASING.md#signature-verification](/RELEASING.md#signature-verification) to learn how to verify them. + ## License Code released under [the MIT license](https://github.com/HathorNetwork/hathor-wallet/blob/dev/LICENSE). diff --git a/RELEASING.md b/RELEASING.md index 313eb9a9..6709d5d6 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,4 +12,114 @@ Create a git tag and a new release on GitHub. # Publishing the new App -## TODO \ No newline at end of file +In case this is a Hathor release, make sure you also read our internal guide in https://github.com/HathorNetwork/ops-tools/blob/master/docs/release-guides/hathor-wallet.md + +1. Run the `release.sh` script, which will clean the environment and build the app for all platforms. The files go to the `dist` folder after the script finishes running. You should get 4 of them: `.AppImage`, `.deb`, `.dmg` and `.exe`. + +1. Generate and concatenate the sha256sum for all 4 files with the following command: + +``` +for file in *.AppImage *.deb *.dmg *.exe; do sha256sum "$file" >> SHA256SUMS; done +``` + +This will generate a SHA256SUMS file containing all hashes, one per line. + +1. Sign the SHA256SUMS file with your GPG key: + +``` +gpg --armor --output SHA256SUMS.asc --detach-sig SHA256SUMS +``` + +This will generate a SHA256SUMS.asc file containing the signature. + +1. Optionally, you can ask other signers to follow the same procedure and concatenate their signatures to the SHA256SUMS.asc file. This way we will have more than one person attesting the files. + +1. Upload the 4 package files, the SHA256SUMS and the SHA256SUMS.asc to the new GitHub release. + +# Signature verification + +To verify the signature of one of our packages, you should download it together with the SHA256SUMS and SHA256SUMS.asc files from the same release. The latest release can be found in https://github.com/HathorNetwork/hathor-wallet/releases/latest. + +Then, you should verify the SHA256SUMS file with: + +``` +sha256sum --ignore-missing --check SHA256SUMS +``` + +In the output produced by the above command, you can safely ignore any warnings and failures, but you must ensure the output lists "OK" after the name of the release file you downloaded. For example: `hathor-wallet_0.26.0_amd64.deb: OK` + +You'll need to import the public GPG key of our signers to verify the signature. Check [Our public keys](#our-public-keys) to see how to add them to your keyring. + +Finally, verify the signature of the SHA256SUMS file with: + +``` +gpg --verify SHA256SUMS.asc SHA256SUMS +``` + +Ideally, you'll see something like: + +``` +gpg: Signature made Fri 09 Oct 2015 05:41:55 PM CEST using RSA key ID 4F25E3B6 +gpg: Good signature from "John Paul (dist sig)" [full] +``` + +Which indicates that the signature is valid. + +If you didn't import the public keys of our signers, you'll get an error like this: + +``` +gpg: Can't check signature: No public key +``` + +If you did import the public keys, you may still get a warning like this: + +``` +gpg: WARNING: This key is not certified with a trusted signature! +gpg: There is no indication that the signature belongs to the owner. +``` + +This means you have a copy of the key and the signature is valid, but either you have not marked the key as trusted or the key is a forgery. In this case, at the very least, you should compare the fingerprints for the signatures in [Our public keys](#our-public-keys) with the fingerprints of the keys you have in your keyring. If they match, you can mark the keys as trusted with: + +``` +gpg --edit-key +$ trust +``` + +## Our public keys + +Current releases are signed by one or more of the keys in [./gpg-keys](./gpg-keys). You should download them all and import them with: + +``` +gpg --import *.pgp +``` + +After doing so, you can get their fingerprints by listing all your keys: + +``` +gpg --list-keys +``` + +Compare the fingerprints with our list below to make sure the keys you have are legit. + +You can optionally mark the keys as trusted with: + +``` +gpg --edit-key +$ trust +``` + +Then choose the trust level you want to give to the key. + +WARNING: Make sure there are no recent commits altering the existing keys in [./gpg-keys](./gpg-keys) or the fingerprint list below. We will not change them often. If there are, you should check the commit history to make sure the commits are signed themselves by someone from Hathor Labs. If you are not sure, please contact us. + +These are the fingerprints of the keys we currently have in the repository: + +``` +``` + +# Adding your GPG key to the repository + +If you want to sign the releases, you should add your GPG key to the repository. To do so, you should open a PR that: + +1. Adds your public key to the [./gpg-keys](./gpg-keys) folder. +1. Adds your fingerprint to the list in [Our public keys](#our-public-keys). \ No newline at end of file diff --git a/scripts/check_version b/scripts/check_version index 21df3e91..82bd7843 100755 --- a/scripts/check_version +++ b/scripts/check_version @@ -3,11 +3,13 @@ SRC_VERSION=`grep "const VERSION " ./src/constants.js | cut -d"'" -f2` ELECTRON_VERSION=`grep "const walletVersion " ./public/electron.js | cut -d "'" -f2` PACKAGE_VERSION=`grep '"version":' ./package.json | cut -d '"' -f4` +PACKAGE_LOCK_VERSION=`node -p "require('./package-lock.json').version"` # For debugging: # echo x${SRC_VERSION}x # echo x${ELECTRON_VERSION}x # echo x${PACKAGE_VERSION}x +# echo x${PACKAGE_LOCK_VERSION}x EXITCODE=0 @@ -21,4 +23,9 @@ if [[ x${PACKAGE_VERSION}x != x${ELECTRON_VERSION}x ]]; then EXITCODE=-1 fi +if [[ x${PACKAGE_VERSION}x != x${PACKAGE_LOCK_VERSION}x ]]; then + echo Version different in package.json and package-lock.json + EXITCODE=-1 +fi + exit $EXITCODE From ef57015015375a477cffd72baf62f4e14baf541a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Fri, 4 Aug 2023 11:25:05 -0300 Subject: [PATCH 06/23] feat: new storage scheme (#404) * feat: new storage scheme --- package-lock.json | 152 ++++++- package.json | 4 +- public/preload.js | 2 +- src/App.js | 75 ++-- src/__tests__/components/ModalPin.test.js | 18 +- src/__tests__/components/ModalSendTx.test.js | 4 +- .../components/WalletAddress.test.js | 31 +- src/__tests__/utils/atomicSwap.test.js | 48 +- src/actions/index.js | 63 +-- src/components/ModalAddManyTokens.js | 11 +- src/components/ModalAddToken.js | 9 +- src/components/ModalBackupWords.js | 22 +- src/components/ModalLedgerSignToken.js | 31 +- src/components/ModalPin.js | 13 +- src/components/ModalResetAllData.js | 31 +- src/components/ModalUnhandledError.js | 2 +- src/components/ModalUnregisteredTokenInfo.js | 18 +- src/components/OutputsWrapper.js | 20 +- src/components/RequestError.js | 4 +- src/components/SendTokensOne.js | 5 +- src/components/TokenAdministrative.js | 8 +- src/components/TokenBar.js | 8 +- src/components/TokenGeneralInfo.js | 2 +- src/components/TokenHistory.js | 6 +- src/components/TxData.js | 71 +-- src/components/Version.js | 8 +- src/components/WalletAddress.js | 10 +- src/components/WalletBalance.js | 3 +- .../atomic-swap/ModalAtomicReceive.js | 15 +- src/components/atomic-swap/ModalAtomicSend.js | 25 +- src/components/tokens/TokenDelegate.js | 2 +- src/components/tokens/TokenDestroy.js | 7 +- src/components/tokens/TokenMelt.js | 4 +- src/components/tokens/TokenMint.js | 10 +- src/reducers/index.js | 10 +- src/sagas/featureToggle.js | 24 +- src/sagas/helpers.js | 40 ++ src/sagas/wallet.js | 152 ++++--- src/screens/AddressList.js | 5 +- src/screens/ChoosePassphrase.js | 22 +- src/screens/CreateNFT.js | 24 +- src/screens/CreateToken.js | 19 +- src/screens/CustomTokens.js | 6 +- src/screens/LoadWallet.js | 9 +- src/screens/LockedWallet.js | 13 +- src/screens/NewWallet.js | 9 +- src/screens/SendTokens.js | 36 +- src/screens/Server.js | 63 ++- src/screens/Settings.js | 7 +- src/screens/StartHardwareWallet.js | 35 +- src/screens/TransactionDetail.js | 2 +- src/screens/UnknownTokens.js | 5 +- src/screens/Wallet.js | 15 +- src/screens/WalletType.js | 5 +- src/screens/WalletVersionError.js | 7 +- src/screens/Welcome.js | 6 +- src/screens/atomic-swap/EditSwap.js | 90 ++-- src/storage.js | 422 ++++++++++++++++-- src/storageInstance.js | 15 - src/utils/atomicSwap.js | 124 ++--- src/utils/helpers.js | 20 +- src/utils/ledger.js | 29 +- src/utils/tokens.js | 103 ++--- src/utils/version.js | 16 +- src/utils/wallet.js | 204 ++++----- tests/env.js | 15 + 66 files changed, 1482 insertions(+), 812 deletions(-) delete mode 100644 src/storageInstance.js create mode 100644 tests/env.js diff --git a/package-lock.json b/package-lock.json index bc758e83..9d625521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1600,18 +1600,31 @@ } }, "@hathor/wallet-lib": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-0.46.1.tgz", - "integrity": "sha512-msLP8Xuqv+IRjcBYeSRs5MO77kJmqJyMTAVfP692JCgGcMzYKV6nsiL61yzmdfaL+sUJAPsYFEJqpEzZvdrdCA==", + "version": "1.0.0-rc8", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.0-rc8.tgz", + "integrity": "sha512-Ti65+yYkSf5TMNHjS+iX4UQUfq2wPCGBufR5BRfq+PU7wAcxftEgSlZat/Qdcent9/fEE59V0bLg7uH1JeYvEQ==", "requires": { "axios": "^0.21.4", "bitcore-lib": "^8.25.10", "bitcore-mnemonic": "^8.25.10", + "buffer": "^6.0.3", "crypto-js": "^3.1.9-1", "isomorphic-ws": "^4.0.1", - "lodash": "^4.17.11", + "level": "^8.0.0", + "lodash": "^4.17.21", "long": "^4.0.0", - "ws": "^7.2.1" + "ws": "^7.5.9" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } } }, "@jest/console": { @@ -3743,6 +3756,31 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "abstract-level": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz", + "integrity": "sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA==", + "requires": { + "buffer": "^6.0.3", + "catering": "^2.1.0", + "is-buffer": "^2.0.5", + "level-supports": "^4.0.0", + "level-transcoder": "^1.0.1", + "module-error": "^1.0.1", + "queue-microtask": "^1.2.3" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5171,9 +5209,9 @@ } }, "bitcore-lib": { - "version": "8.25.28", - "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.28.tgz", - "integrity": "sha512-UrNHh0Ba8GUiHUYRmm2IKlb8eomsbvk/Z6oQdaOPQoLiamiKnu45pAMqtcHg06wMDF8at54oIdoD2WEU+TQujw==", + "version": "8.25.47", + "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.47.tgz", + "integrity": "sha512-qDZr42HuP4P02I8kMGZUx/vvwuDsz8X3rQxXLfM0BtKzlQBcbSM7ycDkDN99Xc5jzpd4fxNQyyFXOmc6owUsrQ==", "requires": { "bech32": "=2.0.0", "bip-schnorr": "=0.6.4", @@ -5186,11 +5224,11 @@ } }, "bitcore-mnemonic": { - "version": "8.25.28", - "resolved": "https://registry.npmjs.org/bitcore-mnemonic/-/bitcore-mnemonic-8.25.28.tgz", - "integrity": "sha512-z4EB7r1JYqJCl31rpfm9qZX24tJ+PPvgAk7xvD6x8pqyv60j7hDTn1zt2mDQu0IxGlGMSnQmGmddZf3UcQXQ8w==", + "version": "8.25.47", + "resolved": "https://registry.npmjs.org/bitcore-mnemonic/-/bitcore-mnemonic-8.25.47.tgz", + "integrity": "sha512-wTa0imZZpFTqwlpyokvU8CNl+YdaIvQIrWKp/0AEL9gPX2vuzBnE+U8Ok6D5lHCnbG6dvmoesmtyf6R3aYI86A==", "requires": { - "bitcore-lib": "^8.25.28", + "bitcore-lib": "^8.25.47", "unorm": "^1.4.1" } }, @@ -5462,6 +5500,17 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, + "browser-level": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browser-level/-/browser-level-1.0.1.tgz", + "integrity": "sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==", + "requires": { + "abstract-level": "^1.0.2", + "catering": "^2.1.1", + "module-error": "^1.0.2", + "run-parallel-limit": "^1.1.0" + } + }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -6095,6 +6144,11 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "catering": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", + "integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==" + }, "catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -6219,6 +6273,18 @@ } } }, + "classic-level": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", + "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", + "requires": { + "abstract-level": "^1.0.2", + "catering": "^2.1.0", + "module-error": "^1.0.1", + "napi-macros": "^2.2.2", + "node-gyp-build": "^4.3.0" + } + }, "clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -11746,6 +11812,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, "is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -13620,6 +13691,40 @@ "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" }, + "level": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/level/-/level-8.0.0.tgz", + "integrity": "sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ==", + "requires": { + "browser-level": "^1.0.1", + "classic-level": "^1.2.0" + } + }, + "level-supports": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz", + "integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==" + }, + "level-transcoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz", + "integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==", + "requires": { + "buffer": "^6.0.3", + "module-error": "^1.0.1" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14506,6 +14611,11 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, + "module-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", + "integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -14579,6 +14689,11 @@ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, + "napi-macros": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -16851,6 +16966,11 @@ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, "raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -18044,6 +18164,14 @@ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" }, + "run-parallel-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz", + "integrity": "sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==", + "requires": { + "queue-microtask": "^1.2.2" + } + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", diff --git a/package.json b/package.json index e04075bc..2b2bc4f8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "version": "0.26.0", "private": true, "dependencies": { - "@hathor/wallet-lib": "^0.46.1", + "@hathor/wallet-lib": "^1.0.0-rc8", "@ledgerhq/hw-transport-node-hid": "^6.27.1", "@sentry/electron": "^3.0.7", "babel-polyfill": "^6.26.0", @@ -63,7 +63,7 @@ "start": "npm-run-all -p watch-css start-js", "build-js": "react-scripts build", "build": "npm-run-all build-css build-js", - "test": "react-scripts test", + "test": "react-scripts test --env=./tests/env.js", "e2e": "cypress run", "eject": "react-scripts eject", "electron": "electron --inspect=5858 .", diff --git a/public/preload.js b/public/preload.js index fe33c924..21e73322 100644 --- a/public/preload.js +++ b/public/preload.js @@ -17,7 +17,7 @@ Sentry.init({ process.once('loaded', () => { // Set closed in localStorage, so user does not open in the wallet page - localStorage.setItem('wallet:closed', true); + localStorage.setItem('localstorage:closed', 'true'); // Sending to main process the information about systray message const systrayMessageChecked = JSON.parse(localStorage.getItem('wallet:systray_message_checked')) === true; diff --git a/src/App.js b/src/App.js index 90e7440f..54c0a298 100644 --- a/src/App.js +++ b/src/App.js @@ -33,28 +33,22 @@ import VersionError from './screens/VersionError'; import WalletVersionError from './screens/WalletVersionError'; import LoadWalletFailed from './screens/LoadWalletFailed'; import version from './utils/version'; -import wallet from './utils/wallet'; import tokens from './utils/tokens'; -import { connect } from "react-redux"; +import { connect } from 'react-redux'; import RequestErrorModal from './components/RequestError'; import store from './store/index'; import createRequestInstance from './api/axiosInstance'; import hathorLib from '@hathor/wallet-lib'; import { IPC_RENDERER, LEDGER_ENABLED } from './constants'; -import STORE from './storageInstance'; -import ModalAlert from './components/ModalAlert'; -import SoftwareWalletWarningMessage from './components/SoftwareWalletWarningMessage'; import AddressList from './screens/AddressList'; import NFTList from './screens/NFTList'; import { updateLedgerClosed } from './actions/index'; import {WALLET_STATUS} from './sagas/wallet'; -import ProposalList from "./screens/atomic-swap/ProposalList"; -import EditSwap from "./screens/atomic-swap/EditSwap"; -import NewSwap from "./screens/atomic-swap/NewSwap"; -import ImportExisting from "./screens/atomic-swap/ImportExisting"; - - -hathorLib.storage.setStore(STORE); +import ProposalList from './screens/atomic-swap/ProposalList'; +import EditSwap from './screens/atomic-swap/EditSwap'; +import NewSwap from './screens/atomic-swap/NewSwap'; +import ImportExisting from './screens/atomic-swap/ImportExisting'; +import LOCAL_STORE from './storage'; const mapStateToProps = (state) => { return { @@ -75,24 +69,35 @@ class Root extends React.Component { componentDidUpdate(prevProps) { // When Ledger device loses connection or the app is closed if (this.props.ledgerClosed && !prevProps.ledgerClosed) { - hathorLib.wallet.lock(); + LOCAL_STORE.lock(); this.props.history.push('/wallet_type/'); } } componentDidMount() { hathorLib.axios.registerNewCreateRequestInstance(createRequestInstance); + // Start the wallet as locked + LOCAL_STORE.lock(); if (IPC_RENDERER) { // Event called when user quits hathor app - IPC_RENDERER.on("ledger:closed", () => { - if (hathorLib.wallet.loaded() && hathorLib.wallet.isHardwareWallet()) { + IPC_RENDERER.on('ledger:closed', async () => { + const storage = LOCAL_STORE.getStorage(); + if ( + storage && + await LOCAL_STORE.isLoaded() && + await storage.isHardwareWallet() + ) { this.props.updateLedgerClosed(true); } }); - IPC_RENDERER.on('ledger:manyTokenSignatureValid', (event, arg) => { - if (hathorLib.wallet.isHardwareWallet()) { + IPC_RENDERER.on('ledger:manyTokenSignatureValid', async (_, arg) => { + const storage = LOCAL_STORE.getStorage(); + if ( + storage && + await storage.isHardwareWallet() + ) { // remove all invalid signatures // arg.data is a list of uids with invalid signatures arg.data.forEach(uid => { @@ -105,8 +110,8 @@ class Root extends React.Component { componentWillUnmount() { if (IPC_RENDERER) { - IPC_RENDERER.removeAllListeners("ledger:closed"); - IPC_RENDERER.removeAllListeners("ledger:manyTokenSignatureValid"); + IPC_RENDERER.removeAllListeners('ledger:closed'); + IPC_RENDERER.removeAllListeners('ledger:manyTokenSignatureValid'); } } @@ -153,9 +158,9 @@ class Root extends React.Component { /* * Validate if version is allowed for the loaded wallet */ -const returnLoadedWalletComponent = (Component, props, rest) => { +const returnLoadedWalletComponent = (Component, props) => { // If was closed and is loaded we need to redirect to locked screen - if (hathorLib.wallet.wasClosed()) { + if (LOCAL_STORE.wasClosed() || LOCAL_STORE.isLocked()) { return ; } @@ -212,21 +217,21 @@ const returnStartedRoute = (Component, props, rest) => { } // The wallet was not yet started, go to Welcome - if (!hathorLib.wallet.started()) { + if (!LOCAL_STORE.wasStarted()) { return ; } // The wallet is already loaded const routeRequiresWalletToBeLoaded = rest.loaded; - if (hathorLib.wallet.loaded()) { + if (LOCAL_STORE.getWalletId()) { // Wallet is locked, go to locked screen - if (hathorLib.wallet.isLocked()) { + if (LOCAL_STORE.isLocked()) { return ; } // Route requires the wallet to be loaded, render it if (routeRequiresWalletToBeLoaded) { - return returnLoadedWalletComponent(Component, props, rest); + return returnLoadedWalletComponent(Component, props); } // Route does not require wallet to be loaded. Redirect to wallet "home" screen @@ -267,15 +272,27 @@ const StartedRoute = ({component: Component, ...rest}) => ( * Return a div grouping the Navigation and the Component */ const returnDefaultComponent = (Component, props) => { + const reduxState = store.getState(); + + if (reduxState.isVersionAllowed === undefined) { + // We already handle all js errors in general and open an error modal to the user + // so there is no need to catch the promise error below + version.checkApiVersion(reduxState.wallet); + } + if (version.checkWalletVersion()) { - if (props.location.pathname === '/locked/' && hathorLib.wallet.isHardwareWallet()) { + if ( + props.location.pathname === '/locked/' && + LOCAL_STORE.isHardwareWallet() + ) { // This will redirect the page to Wallet Type screen - wallet.cleanWallet(); - hathorLib.wallet.unlock(); + LOCAL_STORE.resetStorage(); + // XXX: We are skipping destroying the storage this may help + // recover the storage if the same wallet is started later return ; } else { return ( -
+
diff --git a/src/__tests__/components/ModalPin.test.js b/src/__tests__/components/ModalPin.test.js index 88492179..9aba44d8 100644 --- a/src/__tests__/components/ModalPin.test.js +++ b/src/__tests__/components/ModalPin.test.js @@ -2,7 +2,6 @@ import React from 'react'; import { unmountComponentAtNode } from 'react-dom'; import { act, render, screen } from '@testing-library/react'; import $ from 'jquery' -import hathorLib from '@hathor/wallet-lib'; import { ModalPin } from '../../components/ModalPin'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; @@ -61,11 +60,13 @@ describe('pin validation', () => { const validationPattern = '[0-9]{6}'; const failingPin = 'abc123'; const passingPin = '123321' + const wallet = { checkPin: () => Promise.resolve(false) }; act(() => { render( , container ) @@ -74,9 +75,6 @@ describe('pin validation', () => { expect(new RegExp(validationPattern).test(failingPin)).toStrictEqual(false); expect(new RegExp(validationPattern).test(passingPin)).toStrictEqual(true); - const pinMock = jest.spyOn(hathorLib.wallet, 'isPinCorrect') - .mockImplementation(() => false); - // Gets the input element, types the pin and clicks "Go" /** @type HTMLElement */ const pinInput = screen.getByTestId('pin-input'); @@ -96,16 +94,16 @@ describe('pin validation', () => { // Validating form expect(pinInput.validity.patternMismatch).toStrictEqual(false); expect(pinInput.checkValidity()).toStrictEqual(true); - - pinMock.mockRestore(); }); it('displays error on incorrect pin', async () => { + const wallet = { checkPin: () => Promise.resolve(false) }; act(() => { render( , container ) @@ -118,10 +116,7 @@ describe('pin validation', () => { await userEvent.type(pinInput, '123321'); // Clicks "Go" - const pinMock = jest.spyOn(hathorLib.wallet, 'isPinCorrect') - .mockImplementation(() => false); await userEvent.click(goButton); - pinMock.mockRestore(); // Validates error message const element = screen.getByText('Invalid PIN'); @@ -132,12 +127,14 @@ describe('pin validation', () => { const successCallback = jest.fn(); const closeCallback = jest.fn(); const pinText = '123321'; + const wallet = { checkPin: () => Promise.resolve(true) }; act(() => { render( , container ) @@ -150,10 +147,7 @@ describe('pin validation', () => { await userEvent.type(pinInput, pinText); // Clicks "Go" - const pinMock = jest.spyOn(hathorLib.wallet, 'isPinCorrect') - .mockImplementation(() => true); await userEvent.click(goButton); - pinMock.mockRestore(); // Confirms there is no error message const element = screen.queryByText('Invalid PIN'); diff --git a/src/__tests__/components/ModalSendTx.test.js b/src/__tests__/components/ModalSendTx.test.js index 47b1ac64..cb46a9a6 100644 --- a/src/__tests__/components/ModalSendTx.test.js +++ b/src/__tests__/components/ModalSendTx.test.js @@ -2,10 +2,8 @@ import React from 'react'; import { unmountComponentAtNode } from 'react-dom'; import { act, render, screen } from '@testing-library/react'; import $ from 'jquery' -import hathorLib, { SendTransaction } from '@hathor/wallet-lib'; +import { SendTransaction } from '@hathor/wallet-lib'; import { ModalSendTx } from '../../components/ModalSendTx'; -import { SendTxHandler } from '../../components/SendTxHandler'; -import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import helpers from '../../utils/helpers'; diff --git a/src/__tests__/components/WalletAddress.test.js b/src/__tests__/components/WalletAddress.test.js index 41607929..2e86cb81 100644 --- a/src/__tests__/components/WalletAddress.test.js +++ b/src/__tests__/components/WalletAddress.test.js @@ -1,43 +1,21 @@ import React from 'react'; import { unmountComponentAtNode } from 'react-dom'; import { render, act, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { WalletAddress } from '../../components/WalletAddress'; import * as _ from 'lodash'; -import hathorLib from '@hathor/wallet-lib' const sampleAddress = 'WPhehTyNHTPz954CskfuSgLEfuKXbXeK3f'; -class ModalAddressQRCodeMock extends React.Component { - render() { return
} -} jest.mock('../../components/ModalAddressQRCode', () => () => { const MockName = "modal-address-qrcode-mock"; return ; }); -// Mocking the underlying hathor wallet to better manage the tests -let oldWallet; - let container = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); - - // Replace the oldWallet object with a mock focused on validating wallet type - oldWallet = hathorLib.wallet; - hathorLib.wallet = (() => { - let walletType = 'software'; - - return { - /** @param {'software'|'hardware'} type */ - setMockType: (type) => walletType = type, - - isHardwareWallet: () => walletType === 'hardware', - isSoftwareWallet: () => walletType === 'software', - } - })() }); afterEach(() => { @@ -45,9 +23,6 @@ afterEach(() => { unmountComponentAtNode(container); container.remove(); container = null; - - // Revert mocked oldWallet - hathorLib.wallet = oldWallet; }); describe('rendering tests', () => { @@ -62,6 +37,7 @@ describe('rendering tests', () => { }); it('renders the correct address on a software wallet', () => { + window.localStorage.setItem('localstorage:ishardware', 'false'); render( { }); it('renders the correct address on a hardware wallet', () => { - hathorLib.wallet.setMockType("hardware") + window.localStorage.setItem('localstorage:ishardware', 'true'); render( { }); it('renders the "see all addresses" option on a software wallet', () => { + window.localStorage.setItem('localstorage:ishardware', 'false'); render( { }); it('does not render the "see all addresses" option on a hardware wallet', () => { - hathorLib.wallet.setMockType('hardware') + window.localStorage.setItem('localstorage:ishardware', 'true'); render( mockNetwork, + }, + isAddressMine: async (address) => { + return address.startsWith('mine-'); + }, +} + /** * Mocked wallet to help with the tests * @type {HathorWallet} */ const wallet = { - getNetworkObject: () => ({ name: 'privatenet' }), - isAddressMine: (address) => { + getNetworkObject: () => mockNetwork, + isAddressMine: async (address) => { return address.startsWith('mine-'); }, -}; - -const mockNetwork = { - name: "privatenet", + storage: mockStorage, }; function createNewProposal() { - const np = new PartialTxProposal(mockNetwork); + const np = new PartialTxProposal(mockStorage); // Mock another wallet sending 200 HTR and receiving 1 of a custom token np.partialTx.inputs = [ @@ -64,14 +74,14 @@ describe('calculateExhibitionData', () => { const deserializeSpy = jest.spyOn(PartialTxProposal, 'fromPartialTx'); const fakePartialTx = { serialize: () => 'fakeSerializedPartialTx' }; - it('should return an empty array when there is no interaction with the wallet', () => { + it('should return an empty array when there is no interaction with the wallet', async () => { deserializeSpy.mockImplementationOnce(() => createNewProposal()) const cachedTokens = {}; - const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + const results = await calculateExhibitionData(fakePartialTx, cachedTokens, wallet); expect(results).toStrictEqual([]); }) - it('should return the correct balance for a single receive', () => { + it('should return the correct balance for a single receive', async () => { // Mock receiving 200 HTR deserializeSpy.mockImplementationOnce(() => { const np = createNewProposal(); @@ -90,7 +100,7 @@ describe('calculateExhibitionData', () => { return np; }) const cachedTokens = {}; - const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + const results = await calculateExhibitionData(fakePartialTx, cachedTokens, wallet); expect(results).toStrictEqual([ expect.objectContaining({ tokenUid: '00', @@ -99,7 +109,7 @@ describe('calculateExhibitionData', () => { ]); }) - it('should return the correct balance for a single send', () => { + it('should return the correct balance for a single send', async () => { // Mock sending 1 custom token deserializeSpy.mockImplementationOnce(() => { const np = createNewProposal(); @@ -116,7 +126,7 @@ describe('calculateExhibitionData', () => { return np; }) const cachedTokens = {}; - const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + const results = await calculateExhibitionData(fakePartialTx, cachedTokens, wallet); expect(results).toStrictEqual([ expect.objectContaining({ tokenUid: customTokenUid, @@ -125,7 +135,7 @@ describe('calculateExhibitionData', () => { ]); }) - it('should return the correct balance for sending and receiving multiple tokens', () => { + it('should return the correct balance for sending and receiving multiple tokens', async () => { // Mock sending 1 custom token deserializeSpy.mockImplementationOnce(() => { const np = createNewProposal(); @@ -154,7 +164,7 @@ describe('calculateExhibitionData', () => { return np; }) const cachedTokens = {}; - const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + const results = await calculateExhibitionData(fakePartialTx, cachedTokens, wallet); expect(results).toStrictEqual(expect.arrayContaining([ expect.objectContaining({ tokenUid: customTokenUid, @@ -167,7 +177,7 @@ describe('calculateExhibitionData', () => { ])); }) - it('should return the correct balance for sending and receiving zero tokens', () => { + it('should return the correct balance for sending and receiving zero tokens', async () => { // Mock sending 1 custom token deserializeSpy.mockImplementationOnce(() => { const np = createNewProposal(); @@ -196,7 +206,7 @@ describe('calculateExhibitionData', () => { return np; }) const cachedTokens = {}; - const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + const results = await calculateExhibitionData(fakePartialTx, cachedTokens, wallet); expect(results).toStrictEqual([ expect.objectContaining({ tokenUid: '00', @@ -204,7 +214,7 @@ describe('calculateExhibitionData', () => { ]); }) - it('should return the correct balance for all conditions above simultaneously', () => { + it('should return the correct balance for all conditions above simultaneously', async () => { deserializeSpy.mockImplementationOnce(() => { const np = createNewProposal(); // Token '00' has zero balance @@ -284,7 +294,7 @@ describe('calculateExhibitionData', () => { return np; }) const cachedTokens = {}; - const results = calculateExhibitionData(fakePartialTx, cachedTokens, wallet); + const results = await calculateExhibitionData(fakePartialTx, cachedTokens, wallet); expect(results).toStrictEqual([ expect.objectContaining({ tokenUid: '00', diff --git a/src/actions/index.js b/src/actions/index.js index 4f6193d6..180181f1 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -56,164 +56,165 @@ export const types = { /** * Update transaction history */ -export const historyUpdate = (data) => ({ type: "history_update", payload: data }); +export const historyUpdate = (data) => ({ type: 'history_update', payload: data }); /** * Reload data from localStorage to Redux */ -export const reloadData = data => ({ type: "reload_data", payload: data }); +export const reloadData = data => ({ type: 'reload_data', payload: data }); /** * Clean redux data and set as initialState */ -export const cleanData = () => ({ type: "clean_data" }); +export const cleanData = () => ({ type: 'clean_data' }); /** * Update address that must be shared with user */ -export const sharedAddressUpdate = data => ({ type: "shared_address", payload: data }); +export const sharedAddressUpdate = data => ({ type: 'shared_address', payload: data }); /** * Set if API version is allowed */ -export const isVersionAllowedUpdate = data => ({ type: "is_version_allowed_update", payload: data }); +export const isVersionAllowedUpdate = data => ({ type: 'is_version_allowed_update', payload: data }); /** * Set if websocket is connected */ -export const isOnlineUpdate = data => ({ type: "is_online_update", payload: data }); +export const isOnlineUpdate = data => ({ type: 'is_online_update', payload: data }); /** * Update the network that you are connected */ -export const networkUpdate = data => ({ type: "network_update", payload: data }); +export const networkUpdate = data => ({ type: 'network_update', payload: data }); /** * Save last request that failed */ -export const lastFailedRequest = data => ({ type: "last_failed_request", payload: data }); +export const lastFailedRequest = data => ({ type: 'last_failed_request', payload: data }); /** * Save written password in Redux (it's always cleaned after the use) */ -export const updatePassword = data => ({ type: "update_password", payload: data }); +export const updatePassword = data => ({ type: 'update_password', payload: data }); /** * Save written pin in Redux (it's always cleaned after the use) */ -export const updatePin = data => ({ type: "update_pin", payload: data }); +export const updatePin = data => ({ type: 'update_pin', payload: data }); /** * Save words in Redux (it's always cleaned after the use) */ -export const updateWords = data => ({ type: "update_words", payload: data }); +export const updateWords = data => ({ type: 'update_words', payload: data }); /** * Update token that is selected in the wallet */ -export const selectToken = data => ({ type: "select_token", payload: data }); +export const selectToken = data => ({ type: 'select_token', payload: data }); /** * Update selected token and all known tokens in the wallet */ -export const newTokens = data => ({ type: "new_tokens", payload: data }); +export const newTokens = data => ({ type: 'new_tokens', payload: data }); /** * Set if addresses are being loaded */ -export const loadingAddresses = data => ({ type: "loading_addresses_update", payload: data }); +export const loadingAddresses = data => ({ type: 'loading_addresses_update', payload: data }); /** * Set quantity of addresses and transactions already loaded */ -export const updateLoadedData = data => ({ type: "update_loaded_data", payload: data }); +export const updateLoadedData = data => ({ type: 'update_loaded_data', payload: data }); /** * Update status code of the last request that failed */ -export const updateRequestErrorStatusCode = data => ({ type: "update_request_error_status_code", payload: data }); +export const updateRequestErrorStatusCode = data => ({ type: 'update_request_error_status_code', payload: data }); /** * Set height * height {number} new network height * htrUpdatedBalance {Object} balance of HTR */ -export const updateHeight = (height, htrUpdatedBalance) => ({ type: "update_height", payload: { height, htrUpdatedBalance } }); +export const updateHeight = (height, htrUpdatedBalance) => ({ type: 'update_height', payload: { height, htrUpdatedBalance } }); /** * wallet {HathorWallet} wallet object */ -export const setWallet = (wallet) => ({ type: "set_wallet", payload: wallet }); +export const setWallet = (wallet) => ({ type: 'set_wallet', payload: wallet }); /** * Stop and clean wallet redux state */ -export const resetWallet = () => ({ type: "reset_wallet" }); +export const resetWallet = () => ({ type: 'reset_wallet' }); /** * tokens {Array} array of token uids the the wallet has + * currentAddress {Object} The current unused address */ -export const loadWalletSuccess = (tokens) => ({ type: "load_wallet_success", payload: { tokens } }); +export const loadWalletSuccess = (tokens, currentAddress) => ({ type: 'load_wallet_success', payload: { tokens, currentAddress } }); /** * tx {Object} the new transaction * updatedBalanceMap {Object} balance updated of each token in this tx * balances {Object} balance of each token in this tx for this wallet including authorities */ -export const updateTx = (tx, updatedBalanceMap, balances) => ({ type: "update_tx", payload: { tx, updatedBalanceMap, balances } }); +export const updateTx = (tx, updatedBalanceMap, balances) => ({ type: 'update_tx', payload: { tx, updatedBalanceMap, balances } }); /** * token {String} token of the updated history * newHistory {Array} array with the new fetched history */ -export const updateTokenHistory = (token, newHistory) => ({ type: "update_token_history", payload: { token, newHistory } }); +export const updateTokenHistory = (token, newHistory) => ({ type: 'update_token_history', payload: { token, newHistory } }); /** * data {Object} object with token metadata */ -export const tokenMetadataUpdated = (data, errors) => ({ type: "token_metadata_updated", payload: { data, errors } }); +export const tokenMetadataUpdated = (data, errors) => ({ type: 'token_metadata_updated', payload: { data, errors } }); /** * Set if metadata was already loaded from the lib */ -export const metadataLoaded = data => ({ type: "metadata_loaded", payload: data }); +export const metadataLoaded = data => ({ type: 'metadata_loaded', payload: data }); /** * Remove token metadata after unregister token */ -export const removeTokenMetadata = data => ({ type: "remove_token_metadata", payload: data }); +export const removeTokenMetadata = data => ({ type: 'remove_token_metadata', payload: data }); /** * Partially update history and balance */ -export const partiallyUpdateHistoryAndBalance = (data) => ({ type: "partially_update_history_and_balance", payload: data }); +export const partiallyUpdateHistoryAndBalance = (data) => ({ type: 'partially_update_history_and_balance', payload: data }); /** * Flag indicating if we are using the wallet service facade */ -export const setUseWalletService = (useWalletService) => ({ type: "set_use_wallet_service", payload: useWalletService }); +export const setUseWalletService = (useWalletService) => ({ type: 'set_use_wallet_service', payload: useWalletService }); /** * Action to display the locked wallet screen and resolve the passed promise after the user typed his PIN */ -export const lockWalletForResult = (promise) => ({ type: "lock_wallet_for_result", payload: promise }); +export const lockWalletForResult = (promise) => ({ type: 'lock_wallet_for_result', payload: promise }); /** * This will resolve the promise and reset the lockWalletPromise state */ -export const resolveLockWalletPromise = (pin) => ({ type: "resolve_lock_wallet_promise", payload: pin }); +export const resolveLockWalletPromise = (pin) => ({ type: 'resolve_lock_wallet_promise', payload: pin }); /** * This will reset the selected token if the one selected has been hidden because of zero balance */ -export const resetSelectedTokenIfNeeded = () => ({ type: "reset_selected_token_if_needed" }); +export const resetSelectedTokenIfNeeded = () => ({ type: 'reset_selected_token_if_needed' }); /** * This will be used when the Ledger app closes and after the user is notified. * * @param {boolean} data If the Ledger device has disconnected. */ -export const updateLedgerClosed = data => ({ type: "set_ledger_was_closed", payload: data }); +export const updateLedgerClosed = data => ({ type: 'set_ledger_was_closed', payload: data }); /** * tokenId: The tokenId to request history from diff --git a/src/components/ModalAddManyTokens.js b/src/components/ModalAddManyTokens.js index c867414b..c592519e 100644 --- a/src/components/ModalAddManyTokens.js +++ b/src/components/ModalAddManyTokens.js @@ -10,10 +10,15 @@ import { t } from 'ttag'; import { get } from 'lodash'; import $ from 'jquery'; import hathorLib from '@hathor/wallet-lib'; +import { connect } from 'react-redux'; import tokens from '../utils/tokens'; import wallet from "../utils/wallet"; +const mapStateToProps = (state) => { + return { storage: state.wallet.storage }; +}; + /** * Component that shows a modal to add many unknown tokens to the wallet (bulk import) * @@ -100,7 +105,7 @@ class ModalAddManyTokens extends React.Component { // Preventing when the user forgets a comma in the end if (config !== '') { // Getting all validation promises - validations.push(hathorLib.tokens.validateTokenToAddByConfigurationString(config)); + validations.push(hathorLib.tokensUtils.validateTokenToAddByConfigurationString(config, this.props.storage)); } } @@ -157,7 +162,7 @@ class ModalAddManyTokens extends React.Component { // Adding the tokens to the wallet and returning with the success callback for (const config of tokensToAdd) { - tokens.addToken(config.uid, config.name, config.symbol); + await tokens.addToken(config.uid, config.name, config.symbol); wallet.setTokenAlwaysShow(config.uid, this.state.alwaysShow); } @@ -235,4 +240,4 @@ class ModalAddManyTokens extends React.Component { } } -export default ModalAddManyTokens; +export default connect(mapStateToProps)(ModalAddManyTokens); diff --git a/src/components/ModalAddToken.js b/src/components/ModalAddToken.js index 96a133d9..5de91764 100644 --- a/src/components/ModalAddToken.js +++ b/src/components/ModalAddToken.js @@ -10,9 +10,14 @@ import { t } from 'ttag'; import $ from 'jquery'; import tokens from '../utils/tokens'; import hathorLib from '@hathor/wallet-lib'; +import { connect } from 'react-redux'; import wallet from "../utils/wallet"; +const mapStateToProps = (state) => { + return { storage: state.wallet.storage }; +}; + /** * Component that shows a modal to add one specific unknown token to the wallet * @@ -83,7 +88,7 @@ class ModalAddToken extends React.Component { } try { - const tokenData = await hathorLib.tokens.validateTokenToAddByConfigurationString(this.refs.config.value, null); + const tokenData = await hathorLib.tokensUtils.validateTokenToAddByConfigurationString(this.refs.config.value, this.props.storage); const tokensBalance = this.props.tokensBalance; const tokenUid = tokenData.uid; @@ -190,4 +195,4 @@ class ModalAddToken extends React.Component { } } -export default ModalAddToken; +export default connect(mapStateToProps)(ModalAddToken); diff --git a/src/components/ModalBackupWords.js b/src/components/ModalBackupWords.js index e8b7d2b5..ff546a30 100644 --- a/src/components/ModalBackupWords.js +++ b/src/components/ModalBackupWords.js @@ -24,12 +24,15 @@ const mapDispatchToProps = dispatch => { const mapStateToProps = (state) => { - return { words: state.words }; + return { + words: state.words, + wallet: state.wallet, + }; }; /** - * Component that shows a modal to do backup of words + * Component that shows a modal to do backup of words * If user is already inside the wallet asks for the user password * * @memberof Components @@ -97,19 +100,24 @@ class ModalBackupWords extends React.Component { * * @param {Object} e Event emitted when button is clicked */ - handlePassword = (e) => { + handlePassword = async (e) => { e.preventDefault(); if (this.refs.formPassword.checkValidity() === false) { this.setState({ passwordFormValidated: true }); } else { this.setState({ passwordFormValidated: false }); const password = this.refs.password.value; - if (hathorLib.wallet.isPasswordCorrect(password)) { - const words = hathorLib.wallet.getWalletWords(password); + try { + const accessData = await this.props.wallet.storage.getAccessData(); + const words = hathorLib.cryptoUtils.decryptData(accessData.words, password); this.props.updateWords(words); this.setState({ passwordSuccess: true, errorMessage: '' }); - } else { - this.setState({ errorMessage: t`Invalid password` }); + } catch (err) { + if (err && err.errorCode === hathorLib.ErrorMessages.DECRYPTION_ERROR) { + // If the password is invalid it will throw a DecryptionError + this.setState({ errorMessage: t`Invalid password` }); + } + throw err; } } }; diff --git a/src/components/ModalLedgerSignToken.js b/src/components/ModalLedgerSignToken.js index 46a48330..e9b8ca3a 100644 --- a/src/components/ModalLedgerSignToken.js +++ b/src/components/ModalLedgerSignToken.js @@ -13,6 +13,25 @@ import { IPC_RENDERER } from '../constants'; import { t } from 'ttag'; import LedgerSignTokenInfo from './LedgerSignTokenInfo'; +const LEDGER_ERROR = { + USER_DENY: 0x6985, + WRONG_P1P2: 0x6a86, + SW_WRONG_DATA_LENGTH: 0x6a87, + SW_INS_NOT_SUPPORTED: 0x6d00, + SW_CLA_NOT_SUPPORTED: 0x6e00, + SW_WRONG_RESPONSE_LENGTH: 0xb000, + SW_DISPLAY_BIP32_PATH_FAIL: 0xb001, + SW_DISPLAY_ADDRESS_FAIL: 0xb002, + SW_DISPLAY_AMOUNT_FAIL: 0xb003, + SW_WRONG_TX_LENGTH: 0xb004, + SW_TX_PARSING_FAIL: 0xb005, + SW_TX_HASH_FAIL: 0xb006, + SW_BAD_STATE: 0xb007, + SW_SIGNATURE_FAIL: 0xb008, + SW_INVALID_TX: 0xb009, + SW_INVALID_SIGNATURE: 0xb00a, +} + /** * Component that shows a modal to sign a custom token with ledger * @@ -119,10 +138,10 @@ function ModalLedgerSignToken({token, modalId, cb, onClose}) { } else { switch (arg.error.status) { // user deny - case 0x6985: + case LEDGER_ERROR.USER_DENY: setError('user_deny'); break; - case 0xb00a: + case LEDGER_ERROR.SW_INVALID_SIGNATURE: setError('invalid_token'); break; default: @@ -229,19 +248,19 @@ function ModalLedgerSignToken({token, modalId, cb, onClose}) { const renderErrorMessage = () => { let error = ''; switch (errorMessage) { - case 'signature_fail': + case 'signature_fail': error = t`Signature failed on Ledger`; break; - case 'user_deny': + case 'user_deny': error = t`User denied token on Ledger`; break; case 'invalid_token': error = t`Ledger denied token`; break; - case 'invalid_symbol': + case 'invalid_symbol': error = t`Invalid token symbol`; break; - case 'invalid_name': + case 'invalid_name': error = t`Invalid token name`; break; } diff --git a/src/components/ModalPin.js b/src/components/ModalPin.js index 1a3dd8a1..cfa2bd28 100644 --- a/src/components/ModalPin.js +++ b/src/components/ModalPin.js @@ -9,9 +9,16 @@ import React from 'react'; import { t } from 'ttag'; import $ from 'jquery'; import PinInput from './PinInput'; -import hathorLib from '@hathor/wallet-lib'; +import { connect } from 'react-redux'; import PropTypes from "prop-types"; + +const mapStateToProps = (state) => { + return { + wallet: state.wallet, + }; +}; + /** * Component that shows a modal with a form to ask for the user PIN * and when the PIN succeeds, it invokes the callback function @@ -71,7 +78,7 @@ export class ModalPin extends React.Component { const pin = this.refs.pinInput.refs.pin.value; // Incorrect PIN, show error message and do nothing else - if (!hathorLib.wallet.isPinCorrect(pin)) { + if (!await this.props.wallet.checkPin(pin)) { this.setState({ errorMessage: t`Invalid PIN` }) return; } @@ -129,7 +136,7 @@ export class ModalPin extends React.Component { } } -export default ModalPin; +export default connect(mapStateToProps)(ModalPin); ModalPin.propTypes = { diff --git a/src/components/ModalResetAllData.js b/src/components/ModalResetAllData.js index bcb60e05..26afb9c2 100644 --- a/src/components/ModalResetAllData.js +++ b/src/components/ModalResetAllData.js @@ -8,13 +8,20 @@ import React from 'react'; import { t } from 'ttag'; import $ from 'jquery'; -import hathorLib from '@hathor/wallet-lib'; import { CONFIRM_RESET_MESSAGE } from '../constants'; +import { connect } from 'react-redux'; import SpanFmt from './SpanFmt'; +import LOCAL_STORE from '../storage'; +const mapStateToProps = (state) => { + return { + wallet: state.wallet, + }; +}; + /** - * Component that shows a modal to ask form confirmation data to reset the wallet + * Component that shows a modal to ask form confirmation data to reset the wallet * Asks for the password and for the user to write a sentence saying that really wants to reset * * @memberof Components @@ -37,12 +44,12 @@ class ModalResetAllData extends React.Component { } /** - * Method to be called when user clicks the button to confirm + * Method to be called when user clicks the button to confirm * Validates the form and then calls a method from props to indicate success * * @param {Object} e Event emitted when button is clicked */ - handleConfirm = (e) => { + handleConfirm = async (e) => { e.preventDefault(); // Form is invalid @@ -67,11 +74,13 @@ class ModalResetAllData extends React.Component { return } - // Password was informed and it is incorrect - const correctPassword = hathorLib.wallet.isPasswordCorrect(password) - if (password && !correctPassword) { - this.setState({errorMessage: t`Invalid password`}) - return + if (!forgotPassword) { + // Password was informed and it is incorrect + const correctPassword = await this.props.wallet.checkPassword(password); + if (password && !correctPassword) { + this.setState({errorMessage: t`Invalid password`}) + return + } } // Password was informed and correct OR password was forgotten: we can proceed to wallet reset. @@ -111,7 +120,7 @@ class ModalResetAllData extends React.Component { render() { const getFirstMessage = () => { let firstMessage = t`If you reset your wallet, all data will be deleted, and you will lose access to your tokens. To recover access to your tokens, you will need to import your words again.`; - if (!hathorLib.wallet.isBackupDone()) { + if (!LOCAL_STORE.isBackupDone()) { firstMessage = t`${firstMessage} You still haven't done the backup of your words.`; } return firstMessage; @@ -167,4 +176,4 @@ class ModalResetAllData extends React.Component { } } -export default ModalResetAllData; +export default connect(mapStateToProps)(ModalResetAllData); diff --git a/src/components/ModalUnhandledError.js b/src/components/ModalUnhandledError.js index 2673c44a..1b4e4e50 100644 --- a/src/components/ModalUnhandledError.js +++ b/src/components/ModalUnhandledError.js @@ -19,7 +19,7 @@ const mapDispatchToProps = dispatch => { }; /** - * Component that shows a modal when an unhandled error happens + * Component that shows a modal when an unhandled error happens * Shows a message to the user with a button for him to copy the error or reset the wallet * * @memberof Components diff --git a/src/components/ModalUnregisteredTokenInfo.js b/src/components/ModalUnregisteredTokenInfo.js index 3401b092..ad36e31b 100644 --- a/src/components/ModalUnregisteredTokenInfo.js +++ b/src/components/ModalUnregisteredTokenInfo.js @@ -43,11 +43,11 @@ class ModalUnregisteredTokenInfo extends React.Component { } /** - * Method called when user clicks the button to register the token + * Method called when user clicks the button to register the token * * @param {Object} e Event emitted when user clicks the button */ - register = (e) => { + register = async (e) => { if (!this.props.token) return; e.preventDefault(); @@ -55,20 +55,20 @@ class ModalUnregisteredTokenInfo extends React.Component { const isValid = this.form.current.checkValidity(); this.setState({ formValidated: true, errorMessage: '' }); if (isValid) { - const configurationString = hathorLib.tokens.getConfigurationString( + const configurationString = hathorLib.tokensUtils.getConfigurationString( this.props.token.uid, this.props.token.name, this.props.token.symbol, ); - const promise = hathorLib.tokens.validateTokenToAddByConfigurationString(configurationString, null); - promise.then((tokenData) => { - tokens.addToken(tokenData.uid, tokenData.name, tokenData.symbol); + try { + const tokenData = await hathorLib.tokensUtils.validateTokenToAddByConfigurationString(configurationString, this.props.wallet.storage); + await tokens.addToken(tokenData.uid, tokenData.name, tokenData.symbol); $('#unregisteredTokenInfoModal').modal('hide'); this.props.tokenRegistered(this.props.token); - }, (e) => { - this.setState({ errorMessage: e.message }); - }); + } catch (err) { + this.setState({ errorMessage: err.message }); + } } } diff --git a/src/components/OutputsWrapper.js b/src/components/OutputsWrapper.js index d91ef3e5..aa947e7b 100644 --- a/src/components/OutputsWrapper.js +++ b/src/components/OutputsWrapper.js @@ -9,10 +9,18 @@ import React from 'react'; import { t } from 'ttag'; import $ from 'jquery'; import _ from 'lodash'; +import { connect } from 'react-redux'; import hathorLib from '@hathor/wallet-lib'; import InputNumber from './InputNumber'; +import LOCAL_STORE from '../storage'; +const mapStateToProps = (state) => { + return { + wallet: state.wallet, + }; +}; + /** * Component that wraps the outputs of a token in the Send Tokens screen * @@ -27,6 +35,8 @@ class OutputsWrapper extends React.Component { this.timelock = React.createRef(); this.timelockCheckbox = React.createRef(); this.uniqueID = _.uniqueId() + + props.setRef(this); } /** @@ -49,7 +59,7 @@ class OutputsWrapper extends React.Component { if (this.props.isNFT) { return ; } else { - return ; + return ; } } @@ -60,19 +70,19 @@ class OutputsWrapper extends React.Component {
+ title={LOCAL_STORE.isHardwareWallet() ? t`This feature is disabled for hardware wallet` : t`Timelock`} + disabled={LOCAL_STORE.isHardwareWallet() ? true : null}/>
+ disabled={LOCAL_STORE.isHardwareWallet() ? true : null}/> {this.props.index === 0 ? : null}
); } } -export default OutputsWrapper; +export default connect(mapStateToProps)(OutputsWrapper); diff --git a/src/components/RequestError.js b/src/components/RequestError.js index 2dcb022d..5670733b 100644 --- a/src/components/RequestError.js +++ b/src/components/RequestError.js @@ -99,7 +99,7 @@ class RequestErrorModal extends React.Component { } else { let config = this.props.lastFailedRequest; let axios = createRequestInstance(config.resolve); - hathorLib.helpers.fixAxiosConfig(axios, config); + hathorLib.helpersUtils.fixAxiosConfig(axios, config); axios(config).then((response) => { config.resolve(response.data); }); @@ -161,7 +161,7 @@ class RequestErrorModal extends React.Component { } render() { - const serverURL = hathorLib.helpers.getServerURL(); + const serverURL = hathorLib.config.getServerUrl(); return ( -

Deposit: {tokens.getDepositAmount(this.state.amount)} HTR ({hathorLib.helpers.prettyValue(this.props.htrBalance)} HTR available)

+

Deposit: {tokens.getDepositAmount(this.state.amount, depositPercent)} HTR ({hathorLib.numberUtils.prettyValue(this.props.htrBalance)} HTR available)

{this.state.errorMessage}

diff --git a/src/screens/CustomTokens.js b/src/screens/CustomTokens.js index e13376b2..24511f9b 100644 --- a/src/screens/CustomTokens.js +++ b/src/screens/CustomTokens.js @@ -13,8 +13,8 @@ import HathorAlert from '../components/HathorAlert'; import BackButton from '../components/BackButton'; import { NFT_ENABLED } from '../constants'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; -import hathorLib from '@hathor/wallet-lib'; import { connect } from 'react-redux'; +import LOCAL_STORE from '../storage'; /** * Maps redux state to instance props @@ -63,7 +63,7 @@ class CustomTokens extends React.Component { * Triggered when user clicks to do the create a new token, then redirects to the screen */ createTokenClicked = () => { - if (hathorLib.wallet.isHardwareWallet()) { + if (LOCAL_STORE.isHardwareWallet()) { this.context.showModal(MODAL_TYPES.ALERT_NOT_SUPPORTED); } else { this.props.history.push('/create_token/'); @@ -74,7 +74,7 @@ class CustomTokens extends React.Component { * Triggered when user clicks on the Create NFT button */ createNFTClicked = () => { - if (hathorLib.wallet.isHardwareWallet()) { + if (LOCAL_STORE.isHardwareWallet()) { this.context.showModal(MODAL_TYPES.ALERT_NOT_SUPPORTED); } else { this.props.history.push('/create_nft/'); diff --git a/src/screens/LoadWallet.js b/src/screens/LoadWallet.js index 4bdca448..9c8cea4a 100644 --- a/src/screens/LoadWallet.js +++ b/src/screens/LoadWallet.js @@ -16,6 +16,7 @@ import { updatePassword, updatePin } from '../actions/index'; import { connect } from "react-redux"; import hathorLib from '@hathor/wallet-lib'; import InitialImages from '../components/InitialImages'; +import LOCAL_STORE from '../storage'; const mapStateToProps = (state) => { @@ -32,7 +33,7 @@ const mapDispatchToProps = dispatch => { /** - * Screen used to load a wallet that already exists + * Screen used to load a wallet that already exists * Depending on the state can show: * - Write words component * - Choose password component @@ -61,12 +62,12 @@ class LoadWallet extends React.Component { } /** - * Method called when user clicks the 'Import' button + * Method called when user clicks the 'Import' button * Checks if words are valid and, if true, show component to choose password */ import = () => { const words = this.refs.wordsInput.value.trim(); - const ret = hathorLib.wallet.wordsValid(words); + const ret = hathorLib.walletUtils.wordsValid(words); if (ret.valid) { // Using ret.words because this method returns a string with all words // separated by a single space, after removing duplicate spaces and possible break lines @@ -92,7 +93,7 @@ class LoadWallet extends React.Component { const { pin, password } = this.props; // First we clean what can still be there of a last wallet wallet.generateWallet(this.state.words, '', pin, password, this.props.history); - hathorLib.wallet.markBackupAsDone(); + LOCAL_STORE.markBackupDone(); // Clean pin and password from redux this.props.updatePassword(null); this.props.updatePin(null); diff --git a/src/screens/LockedWallet.js b/src/screens/LockedWallet.js index 2596ec26..a5dc3676 100644 --- a/src/screens/LockedWallet.js +++ b/src/screens/LockedWallet.js @@ -10,11 +10,11 @@ import { t } from 'ttag'; import { connect } from "react-redux"; import wallet from '../utils/wallet'; import RequestErrorModal from '../components/RequestError'; -import hathorLib from '@hathor/wallet-lib'; import ReactLoading from 'react-loading'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; import { resolveLockWalletPromise, startWalletRequested, walletReset } from '../actions'; import colors from '../index.scss'; +import LOCAL_STORE from '../storage'; const mapStateToProps = (state) => { @@ -58,12 +58,12 @@ class LockedWallet extends React.Component { } /** - * When user clicks on the unlock button + * When user clicks on the unlock button * Checks if form is valid and if PIN is correct, then unlocks the wallet loading the data and redirecting * * @param {Object} e Event of when the button is clicked */ - unlockClicked = (e) => { + unlockClicked = async (e) => { e.preventDefault(); if (this.state.loading) { @@ -74,11 +74,13 @@ class LockedWallet extends React.Component { if (isValid) { const pin = this.refs.pin.value; this.refs.unlockForm.classList.remove('was-validated') - if (!hathorLib.wallet.isPinCorrect(pin)) { + if (!await LOCAL_STORE.checkPin(pin)) { this.setState({ errorMessage: t`Invalid PIN` }); return; } + await LOCAL_STORE.handleDataMigration(pin); + // LockedWallet screen was called for a result, so we should resolve the promise with the PIN after // it is validated. if (this.props.lockWalletPromise) { @@ -92,12 +94,9 @@ class LockedWallet extends React.Component { loading: true, }); - // Start the wallet from an xpriv that's already encrypted in localStorage. - // Because of that we don't need to send the seed neither the password. this.props.startWallet({ pin, routerHistory: this.props.history, - fromXpriv: true, }); } else { this.refs.unlockForm.classList.add('was-validated') diff --git a/src/screens/NewWallet.js b/src/screens/NewWallet.js index adeac45f..464867ec 100644 --- a/src/screens/NewWallet.js +++ b/src/screens/NewWallet.js @@ -11,7 +11,6 @@ import { t } from 'ttag' import wallet from '../utils/wallet'; import logo from '../assets/images/hathor-logo.png'; import ChoosePassword from '../components/ChoosePassword'; -import ModalBackupWords from '../components/ModalBackupWords'; import ChoosePin from '../components/ChoosePin'; import HathorAlert from '../components/HathorAlert'; import { updatePassword, updatePin, updateWords } from '../actions/index'; @@ -19,7 +18,7 @@ import { connect } from "react-redux"; import hathorLib from '@hathor/wallet-lib'; import InitialImages from '../components/InitialImages'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; -import $ from 'jquery'; +import LOCAL_STORE from '../storage'; const mapDispatchToProps = dispatch => { @@ -67,14 +66,14 @@ class NewWallet extends React.Component { } componentDidMount = () => { - hathorLib.wallet.markBackupAsNotDone(); + LOCAL_STORE.markBackupAsNotDone(); } create = () => { let isValid = this.refs.confirmForm.checkValidity(); if (isValid) { this.refs.confirmForm.classList.remove('was-validated') - const words = hathorLib.wallet.generateWalletWords(hathorLib.constants.HD_WALLET_ENTROPY); + const words = hathorLib.walletUtils.generateWalletWords(hathorLib.constants.HD_WALLET_ENTROPY); this.props.updateWords(words); this.setState({ step2: true }); } else { @@ -127,7 +126,7 @@ class NewWallet extends React.Component { */ validationSuccess = () => { this.context.hideModal(); - hathorLib.wallet.markBackupAsDone(); + LOCAL_STORE.markBackupDone(); this.alertSuccessRef.current.show(3000); this.setState({ askPassword: true }); } diff --git a/src/screens/SendTokens.js b/src/screens/SendTokens.js index 676a2a4e..82705e69 100644 --- a/src/screens/SendTokens.js +++ b/src/screens/SendTokens.js @@ -20,6 +20,7 @@ import { IPC_RENDERER, LEDGER_TX_CUSTOM_TOKEN_LIMIT } from '../constants'; import ReactLoading from 'react-loading'; import colors from '../index.scss'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; +import LOCAL_STORE from '../storage'; const mapStateToProps = (state) => { return { @@ -216,8 +217,10 @@ class SendTokens extends React.Component { * Execute ledger get signatures */ getSignatures = () => { - const keys = hathorLib.wallet.getWalletData().keys; - ledger.getSignatures(Object.assign({}, this.data), keys); + ledger.getSignatures( + Object.assign({}, this.data), + this.props.wallet.storage, + ); } /** @@ -252,7 +255,7 @@ class SendTokens extends React.Component { */ beforeSendLedger = () => { // remove HTR if present - const txTokens = this.state.txTokens.filter(t => !hathorLib.tokens.isHathorToken(t.uid)); + const txTokens = this.state.txTokens.filter(t => !hathorLib.tokensUtils.isHathorToken(t.uid)); if (txTokens.length === 0) { // no custom tokens, just send @@ -290,40 +293,37 @@ class SendTokens extends React.Component { * Method executed to send transaction on ledger * It opens the ledger modal to wait for user action on the device */ - executeSendLedger = () => { + executeSendLedger = async () => { // Wallet Service currently does not support Ledger, so we default to the regular SendTransaction this.sendTransaction = new hathorLib.SendTransaction({ outputs: this.data.outputs, inputs: this.data.inputs, - network: this.props.wallet.getNetworkObject(), + storage: this.props.wallet.storage, }); try { // Errors may happen in this step ( ex.: insufficient amount of tokens ) - this.data = this.sendTransaction.prepareTxData(); + this.data = await this.sendTransaction.prepareTxData(); } catch (e) { this.setState({ errorMessage: e.message, ledgerStep: 0 }) return; } - // Complete data with default values - hathorLib.transaction.completeTx(this.data); - const changeInfo = []; - const keys = hathorLib.wallet.getWalletData().keys; - this.data.outputs.forEach((output, outputIndex) => { + for (const [outputIndex, output] of this.data.outputs.entries()) { if (output.isChange) { changeInfo.push({ outputIndex, - keyIndex: keys[output.address].index, + keyIndex: await this.props.wallet.getAddressIndex(output.address), }); } - }); + } const useOldProtocol = !version.isLedgerCustomTokenAllowed(); - ledger.sendTx(this.data, changeInfo, useOldProtocol); + const network = this.props.wallet.getNetworkObject(); + ledger.sendTx(this.data, changeInfo, useOldProtocol, network); this.context.showModal(MODAL_TYPES.ALERT, { title: t`Validate outputs on Ledger`, body: this.renderAlertBody(), @@ -343,7 +343,7 @@ class SendTokens extends React.Component { this.setState({ errorMessage: '' }); try { this.data = data; - if (hathorLib.wallet.isSoftwareWallet()) { + if (!LOCAL_STORE.isHardwareWallet()) { this.context.showModal(MODAL_TYPES.PIN, { onSuccess: ({pin}) => { this.context.showModal(MODAL_TYPES.SEND_TX, { @@ -379,7 +379,7 @@ class SendTokens extends React.Component { }); } - return new hathorLib.SendTransaction({ outputs: this.data.outputs, inputs: this.data.inputs, pin, network: this.props.wallet.getNetworkObject() }); + return new hathorLib.SendTransaction({ outputs: this.data.outputs, inputs: this.data.inputs, pin, storage: this.props.wallet.storage }); } /** @@ -415,7 +415,7 @@ class SendTokens extends React.Component { * Create a new child reference with this new token */ addAnotherToken = () => { - if (hathorLib.wallet.isHardwareWallet()) { + if (LOCAL_STORE.isHardwareWallet()) { if (!version.isLedgerCustomTokenAllowed()) { // Custom token not allowed for this Ledger version this.context.showModal(MODAL_TYPES.ALERT_NOT_SUPPORTED, { @@ -427,7 +427,7 @@ class SendTokens extends React.Component { }) return; } - if (this.state.txTokens.filter(t => !hathorLib.tokens.isHathorToken(t.uid)).length === LEDGER_TX_CUSTOM_TOKEN_LIMIT) { + if (this.state.txTokens.filter(t => !hathorLib.tokensUtils.isHathorToken(t.uid)).length === LEDGER_TX_CUSTOM_TOKEN_LIMIT) { // limit is 10 custom tokens per tx const modalBody =

{t`Ledger has a limit of ${LEDGER_TX_CUSTOM_TOKEN_LIMIT} different tokens per transaction.`}

this.context.showModal(MODAL_TYPES.ALERT, { diff --git a/src/screens/Server.js b/src/screens/Server.js index 5e571c78..e19b5194 100644 --- a/src/screens/Server.js +++ b/src/screens/Server.js @@ -7,7 +7,6 @@ import React from 'react'; import { t } from 'ttag'; -import SpanFmt from '../components/SpanFmt'; import $ from 'jquery'; import wallet from '../utils/wallet'; import helpers from '../utils/helpers'; @@ -21,6 +20,7 @@ import { import colors from '../index.scss'; import { connect } from "react-redux"; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; +import LOCAL_STORE from '../storage'; const mapStateToProps = (state) => { return { @@ -73,7 +73,7 @@ class Server extends React.Component { } /** - * Called after user click the button to change the server + * Called after user click the button to change the server * Check if form is valid and then reload that from new server */ serverSelected = async () => { @@ -125,9 +125,11 @@ class Server extends React.Component { } // we don't ask for the pin on the hardware wallet - if (hathorLib.wallet.isSoftwareWallet() && !hathorLib.wallet.isPinCorrect(this.refs.pin.value)) { - this.setState({ errorMessage: t`Invalid PIN` }); - return; + if (!LOCAL_STORE.isHardwareWallet()) { + if (!await this.props.wallet.checkPin(this.refs.pin.value)) { + this.setState({ errorMessage: t`Invalid PIN` }); + return; + } } this.setState({ @@ -138,7 +140,7 @@ class Server extends React.Component { }); const currentServer = this.props.useWalletService ? - hathorLib.config.getWalletServiceBaseUrl() : + hathorLib.config.getWalletServiceBaseUrl() : hathorLib.config.getServerUrl(); const currentWsServer = this.props.useWalletService ? @@ -146,13 +148,18 @@ class Server extends React.Component { ''; // Update new server in storage and in the config singleton - this.props.wallet.changeServer(newBaseServer); + await this.props.wallet.changeServer(newBaseServer); // We only have a different websocket server on the wallet-service facade, so update the config singleton if (this.props.useWalletService) { - this.props.wallet.changeWsServer(newWsServer); + await this.props.wallet.changeWsServer(newWsServer); } + LOCAL_STORE.setServers( + newBaseServer, + this.props.useWalletService ? newWsServer : null, + ); + try { const versionData = await this.props.wallet.getVersionData(); @@ -171,10 +178,14 @@ class Server extends React.Component { // Go back to the previous server // If the user decides to continue with this change, we will update again - this.props.wallet.changeServer(currentServer); + await this.props.wallet.changeServer(currentServer); if (this.props.useWalletService) { - this.props.wallet.changeWsServer(currentWsServer); + await this.props.wallet.changeWsServer(currentWsServer); } + LOCAL_STORE.setServers( + currentServer, + this.props.useWalletService ? currentWsServer : null, + ); this.context.showModal(MODAL_TYPES.CONFIRM_TESTNET, { success: this.confirmTestnetServer, }); @@ -187,10 +198,14 @@ class Server extends React.Component { } } catch (e) { // Go back to the previous server - this.props.wallet.changeServer(currentServer); + await this.props.wallet.changeServer(currentServer); if (this.props.useWalletService) { - this.props.wallet.changeWsServer(currentWsServer); + await this.props.wallet.changeWsServer(currentWsServer); } + LOCAL_STORE.setServers( + currentServer, + this.props.useWalletService ? currentWsServer : null, + ); this.setState({ loading: false, errorMessage: e.message, @@ -203,11 +218,15 @@ class Server extends React.Component { * we successfully validated that the user has written 'testnet' on the input * so we can execute the change */ - confirmTestnetServer = () => { - this.props.wallet.changeServer(this.state.selectedServer); + confirmTestnetServer = async () => { + await this.props.wallet.changeServer(this.state.selectedServer); if (this.props.useWalletService) { - this.props.wallet.changeWsServer(this.state.selectedWsServer); + await this.props.wallet.changeWsServer(this.state.selectedWsServer); } + LOCAL_STORE.setServers( + this.state.selectedServer, + this.props.useWalletService ? this.state.selectedWsServer : null, + ); // Set network on config singleton so the load wallet will get it properly hathorLib.config.setNetwork(this.state.selectedNetwork); @@ -224,18 +243,18 @@ class Server extends React.Component { * Execute server change checking server API and, in case of success * reloads data and redirects to wallet screen */ - executeServerChange = () => { + executeServerChange = async () => { // We don't have PIN on hardware wallet - const pin = hathorLib.wallet.isSoftwareWallet() ? this.refs.pin.value : null; - const promise = wallet.changeServer(this.props.wallet, pin, this.props.history); - promise.then(() => { + const pin = LOCAL_STORE.isHardwareWallet() ? null : this.refs.pin.value; + try { + await wallet.changeServer(this.props.wallet, pin, this.props.history); this.props.history.push('/wallet/'); - }, (err) => { + } catch (err) { this.setState({ loading: false, errorMessage: err.message, }); - }); + } } /** @@ -363,7 +382,7 @@ class Server extends React.Component { ) }
- {hathorLib.wallet.isSoftwareWallet() && } + {(!LOCAL_STORE.isHardwareWallet()) && }
diff --git a/src/screens/Settings.js b/src/screens/Settings.js index edd12bf2..76ea174a 100644 --- a/src/screens/Settings.js +++ b/src/screens/Settings.js @@ -21,6 +21,7 @@ import { connect } from "react-redux"; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; import { PRIVACY_POLICY_URL, TERMS_OF_SERVICE_URL } from '../constants'; import { walletReset } from '../actions'; +import LOCAL_STORE from '../storage'; const mapStateToProps = (state) => { return { @@ -103,7 +104,7 @@ class Settings extends React.Component { * When user clicks Add Passphrase button we redirect to Passphrase screen */ addPassphrase = () => { - if (hathorLib.wallet.isHardwareWallet()) { + if (LOCAL_STORE.isHardwareWallet()) { this.context.showModal(MODAL_TYPES.ALERT_NOT_SUPPORTED, { title: t`Complete action on your hardware wallet`, children: ( @@ -132,7 +133,7 @@ class Settings extends React.Component { const configurationStrings = this.props.registeredTokens.filter((token) => { return token.uid !== hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; }).map((token) => { - return hathorLib.tokens.getConfigurationString(token.uid, token.name, token.symbol); + return hathorLib.tokensUtils.getConfigurationString(token.uid, token.name, token.symbol); }); // The text will be all the configuration strings, one for each line @@ -314,7 +315,7 @@ class Settings extends React.Component { render() { const serverURL = this.props.useWalletService ? hathorLib.config.getWalletServiceBaseUrl() : hathorLib.config.getServerUrl(); const wsServerURL = this.props.useWalletService ? hathorLib.config.getWalletServiceBaseWsUrl() : ''; - const ledgerCustomTokens = hathorLib.wallet.isHardwareWallet() && version.isLedgerCustomTokenAllowed(); + const ledgerCustomTokens = (!LOCAL_STORE.isHardwareWallet()) && version.isLedgerCustomTokenAllowed(); const uniqueIdentifier = helpers.getUniqueId(); return ( diff --git a/src/screens/StartHardwareWallet.js b/src/screens/StartHardwareWallet.js index 38a4fc02..e72e157e 100644 --- a/src/screens/StartHardwareWallet.js +++ b/src/screens/StartHardwareWallet.js @@ -10,14 +10,13 @@ import { t } from 'ttag' import { connect } from 'react-redux'; import logo from '../assets/images/hathor-logo.png'; -import wallet from '../utils/wallet'; import ledger from '../utils/ledger'; -import version from '../utils/version'; import helpers from '../utils/helpers'; import { LEDGER_GUIDE_URL, IPC_RENDERER, LEDGER_MIN_VERSION, LEDGER_MAX_VERSION } from '../constants'; import { startWalletRequested } from '../actions'; import InitialImages from '../components/InitialImages'; import hathorLib from '@hathor/wallet-lib'; +import LOCAL_STORE from '../storage'; const mapDispatchToProps = dispatch => { return { @@ -75,7 +74,7 @@ class StartHardwareWallet extends React.Component { if (arg.success) { // compare ledger version with our min version const version = Buffer.from(arg.data).slice(3, 6).join('.'); - hathorLib.storage.setItem('ledger:version', version); + LOCAL_STORE.saveLedgerAppVersion(version); if ( helpers.cmpVersionString(version, LEDGER_MIN_VERSION) < 0 || helpers.cmpVersionString(version, LEDGER_MAX_VERSION) >= 0 @@ -114,42 +113,22 @@ class StartHardwareWallet extends React.Component { if (arg.success) { const data = Buffer.from(arg.data); const uncompressedPubkey = data.slice(0, 65); - const compressedPubkey = hathorLib.wallet.toPubkeyCompressed(uncompressedPubkey); + const compressedPubkey = hathorLib.walletUtils.toPubkeyCompressed(uncompressedPubkey); const chainCode = data.slice(65, 97); const fingerprint = data.slice(97, 101); - const xpub = hathorLib.wallet.xpubFromData(compressedPubkey, chainCode, fingerprint); + const xpub = hathorLib.walletUtils.xpubFromData(compressedPubkey, chainCode, fingerprint); - hathorLib.wallet.setWalletType('hardware'); + LOCAL_STORE.setHardwareWallet(true); this.props.startWallet({ words: null, passphrase: '', pin: null, password: '', routerHistory: this.props.history, - fromXpriv: false, xpub, + hardware: true, }); - hathorLib.wallet.markBackupAsDone(); - - const tokenSignatures = hathorLib.storage.getItem('wallet:token:signatures'); - if (tokenSignatures) { - const dataToken = hathorLib.tokens.getTokens(); - const tokensToVerify = dataToken - .filter(t => tokenSignatures[t.uid] != undefined) - .map(t => { - const signature = tokenSignatures[t.uid]; - return { - uid: t.uid, - name: t.name, - symbol: t.symbol, - signature: signature, - }; - }); - - if (hathorLib.wallet.isHardwareWallet() && version.isLedgerCustomTokenAllowed() && tokensToVerify.length !== 0) { - ledger.verifyManyTokenSignatures(tokensToVerify); - } - } + LOCAL_STORE.markBackupDone(); this.props.history.push('/wallet/'); } else { diff --git a/src/screens/TransactionDetail.js b/src/screens/TransactionDetail.js index a929837c..8143e836 100644 --- a/src/screens/TransactionDetail.js +++ b/src/screens/TransactionDetail.js @@ -95,7 +95,7 @@ class TransactionDetail extends React.Component { try { const data = await this.props.wallet.getFullTxById(this.props.match.params.id); - if (!hathorLib.helpers.isBlock(data.tx)) { + if (!hathorLib.transactionUtils.isBlock(data.tx)) { this.getConfirmationData(); } diff --git a/src/screens/UnknownTokens.js b/src/screens/UnknownTokens.js index f500fa91..512254bd 100644 --- a/src/screens/UnknownTokens.js +++ b/src/screens/UnknownTokens.js @@ -9,7 +9,6 @@ import React from 'react'; import { t } from 'ttag'; import { connect } from 'react-redux'; import { get } from 'lodash'; -import hathorLib from '@hathor/wallet-lib'; import $ from 'jquery'; import HathorAlert from '../components/HathorAlert'; import TokenHistory from '../components/TokenHistory'; @@ -154,7 +153,7 @@ class UnknownTokens extends React.Component { */ massiveImportSuccess = (count) => { this.context.hideModal(); - const message = `${count} ${hathorLib.helpers.plural(count, 'token was', 'tokens were')} added!`; + const message = `${count} ${helpers.plural(count, 'token was', 'tokens were')} added!`; this.setState({ successMessage: message }, () => { this.alertSuccessRef.current.show(3000); }); @@ -251,7 +250,7 @@ class UnknownTokens extends React.Component { const renderTokens = () => { if (unknownTokens.length === 0) { return

{t`You don't have any unknown tokens`}

; - } + } return unknownTokens.map((token, index) => { const isNFT = helpers.isTokenNFT(token.uid, this.props.tokenMetadata); diff --git a/src/screens/Wallet.js b/src/screens/Wallet.js index 17795260..dc62addd 100644 --- a/src/screens/Wallet.js +++ b/src/screens/Wallet.js @@ -31,6 +31,7 @@ import { tokenFetchHistoryRequested, tokenFetchBalanceRequested, } from '../actions/index'; +import LOCAL_STORE from '../storage'; const mapDispatchToProps = dispatch => { @@ -91,7 +92,7 @@ class Wallet extends React.Component { componentDidMount = async () => { this.setState({ - backupDone: hathorLib.wallet.isBackupDone() + backupDone: LOCAL_STORE.isBackupDone() }); this.initializeWalletScreen(); @@ -209,7 +210,7 @@ class Wallet extends React.Component { */ backupSuccess = () => { this.context.hideModal(); - hathorLib.wallet.markBackupAsDone(); + LOCAL_STORE.markBackupDone(); this.props.updateWords(null); this.setState({ backupDone: true, successMessage: t`Backup completed!` }, () => { @@ -236,7 +237,7 @@ class Wallet extends React.Component { signClicked = () => { const token = this.props.tokens.find((token) => token.uid === this.props.selectedToken); - if (hathorLib.wallet.isHardwareWallet() && version.isLedgerCustomTokenAllowed()) { + if (LOCAL_STORE.isHardwareWallet() && version.isLedgerCustomTokenAllowed()) { this.context.showModal(MODAL_TYPES.LEDGER_SIGN_TOKEN, { token, modalId: 'signTokenDataModal', @@ -392,7 +393,7 @@ class Wallet extends React.Component { } const renderTokenData = (token) => { - if (hathorLib.tokens.isHathorToken(this.props.selectedToken)) { + if (hathorLib.tokensUtils.isHathorToken(this.props.selectedToken)) { return renderWallet(); } else { return ( @@ -448,7 +449,7 @@ class Wallet extends React.Component { const renderSignTokenIcon = () => { // Don't show if it's HTR - if (hathorLib.tokens.isHathorToken(this.props.selectedToken)) return null; + if (hathorLib.tokensUtils.isHathorToken(this.props.selectedToken)) return null; const signature = tokens.getTokenSignature(this.props.selectedToken); // Show only if we don't have a signature on storage @@ -496,8 +497,8 @@ class Wallet extends React.Component {

{token ? token.name : ''} - {!hathorLib.tokens.isHathorToken(this.props.selectedToken) && } - {hathorLib.wallet.isHardwareWallet() && version.isLedgerCustomTokenAllowed() && renderSignTokenIcon()} + {!hathorLib.tokensUtils.isHathorToken(this.props.selectedToken) && } + {LOCAL_STORE.isHardwareWallet() && version.isLedgerCustomTokenAllowed() && renderSignTokenIcon()}

{renderTokenData(token)} diff --git a/src/screens/WalletType.js b/src/screens/WalletType.js index cfb8b6d8..407c5748 100644 --- a/src/screens/WalletType.js +++ b/src/screens/WalletType.js @@ -10,14 +10,13 @@ import { t } from 'ttag' import logo from '../assets/images/hathor-logo.png'; import wallet from '../utils/wallet'; -import { HATHOR_WEBSITE_URL } from '../constants'; import SpanFmt from '../components/SpanFmt'; import InitialImages from '../components/InitialImages'; import HathorAlert from '../components/HathorAlert'; import { str2jsx } from '../utils/i18n'; import { connect } from "react-redux"; -import hathorLib from '@hathor/wallet-lib'; import { updateLedgerClosed } from '../actions/index'; +import LOCAL_STORE from '../storage'; const mapStateToProps = (state) => { return { @@ -51,7 +50,7 @@ class WalletType extends React.Component { * Go to software wallet warning screen */ goToSoftwareWallet = () => { - hathorLib.wallet.setWalletType('software'); + LOCAL_STORE.setHardwareWallet(false); this.props.history.push('/software_warning/'); } diff --git a/src/screens/WalletVersionError.js b/src/screens/WalletVersionError.js index 5e9b8d8d..4c061973 100644 --- a/src/screens/WalletVersionError.js +++ b/src/screens/WalletVersionError.js @@ -8,13 +8,12 @@ import React from 'react'; import { t } from 'ttag'; import logo from '../assets/images/hathor-white-logo.png'; -import wallet from '../utils/wallet'; import Version from '../components/Version'; import HathorAlert from '../components/HathorAlert'; import { updateWords, walletReset } from '../actions/index'; import { connect } from "react-redux"; -import hathorLib from '@hathor/wallet-lib'; import { GlobalModalContext, MODAL_TYPES } from '../components/GlobalModal'; +import LOCAL_STORE from '../storage'; const mapDispatchToProps = dispatch => { @@ -26,7 +25,7 @@ const mapDispatchToProps = dispatch => { /** - * Screen used when the previous wallet version installed is not compatible with the new version + * Screen used when the previous wallet version installed is not compatible with the new version * So user can reset the wallet, or install the previous version again * * @memberof Screens @@ -58,7 +57,7 @@ class WalletVersionError extends React.Component { */ backupSuccess = () => { this.context.hideModal(); - hathorLib.wallet.markBackupAsDone(); + LOCAL_STORE.markBackupDone(); this.props.updateWords(null); this.alertSuccessRef.current.show(3000); } diff --git a/src/screens/Welcome.js b/src/screens/Welcome.js index e4b0acbc..0871dbe0 100644 --- a/src/screens/Welcome.js +++ b/src/screens/Welcome.js @@ -10,12 +10,12 @@ import { t } from 'ttag' import SpanFmt from '../components/SpanFmt'; import logo from '../assets/images/hathor-logo.png'; -import hathorLib from '@hathor/wallet-lib'; import wallet from '../utils/wallet'; import InitialImages from '../components/InitialImages'; import { LEDGER_ENABLED, TERMS_OF_SERVICE_URL, PRIVACY_POLICY_URL } from '../constants'; import { str2jsx } from '../utils/i18n'; import helpers from '../utils/helpers'; +import LOCAL_STORE from '../storage'; /** @@ -36,13 +36,13 @@ class Welcome extends React.Component { const isValid = this.refs.agreeForm.checkValidity(); this.setState({ formValidated: !isValid }); if (isValid) { - hathorLib.wallet.markWalletAsStarted(); + LOCAL_STORE.markWalletAsStarted(); // For the mainnet sentry will be disabled by default and the user can change this on Settings wallet.disallowSentry(); if (LEDGER_ENABLED) { this.props.history.push('/wallet_type/'); } else { - hathorLib.wallet.setWalletType('software'); + LOCAL_STORE.setHardwareWallet(false); this.props.history.push('/signin/'); } } diff --git a/src/screens/atomic-swap/EditSwap.js b/src/screens/atomic-swap/EditSwap.js index f100d427..2b5eb748 100644 --- a/src/screens/atomic-swap/EditSwap.js +++ b/src/screens/atomic-swap/EditSwap.js @@ -59,11 +59,9 @@ export default function EditSwap(props) { * The proposal editing will happen entirely on a local state, without interfering on the global * state. Only when explicitly saving will this persistence happen. */ - const [partialTx, setPartialTx] = useState(deserializePartialTx(proposal.data.partialTx, wallet, proposal.data.signatures)); - const [txBalances, setTxBalances] = useState(calculateExhibitionData(partialTx, tokensCache, wallet)); - const [signaturesObj, setSignaturesObj] = useState(proposal.data.signatures && proposal.data.signatures.length - ? calculateSignaturesObject(partialTx, proposal.data.signatures) - : null); + const [partialTx, setPartialTx] = useState(new PartialTx(wallet.getNetworkObject())); + const [txBalances, setTxBalances] = useState([]); + const [signaturesObj, setSignaturesObj] = useState(null); const [hasAtLeastOneSig, setHasAtLeastOneSig] = useState(false); const dispatch = useDispatch(); @@ -246,24 +244,23 @@ export default function EditSwap(props) { * @param {number} amount * @param {Utxo[]} utxos */ - const sendOperationHandler = ({ selectedToken, changeAddress, amount, utxos }) => { - const network = wallet.getNetworkObject(); - const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), network); + const sendOperationHandler = async ({ selectedToken, changeAddress, amount, utxos }) => { + const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), wallet.storage); // Tokens added here will not be marked as selected, only when saved/updated - txProposal.addSend(wallet, selectedToken.uid, amount, { + await txProposal.addSend(selectedToken.uid, amount, { utxos, changeAddress: changeAddress, markAsSelected: false }); - enrichTxData(txProposal.partialTx, wallet); + await enrichTxData(txProposal.partialTx, wallet); setPartialTx(txProposal.partialTx); setSignaturesObj(null); setHasTxChange(true); setHasSigChange(true); - setTxBalances(calculateExhibitionData(txProposal.partialTx, tokensCache, wallet)); + setTxBalances(await calculateExhibitionData(txProposal.partialTx, tokensCache, wallet)); } /** @@ -272,16 +269,15 @@ export default function EditSwap(props) { * @param {string} address * @param {number} amount */ - const receiveOperationHandler = ({ selectedToken, address, amount }) => { - const network = wallet.getNetworkObject(); - const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), network); - txProposal.addReceive(wallet, selectedToken.uid, amount, { address }); - enrichTxData(txProposal.partialTx, wallet); + const receiveOperationHandler = async ({ selectedToken, address, amount }) => { + const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), wallet.storage); + await txProposal.addReceive(selectedToken.uid, amount, { address }); + await enrichTxData(txProposal.partialTx, wallet); setPartialTx(txProposal.partialTx); setSignaturesObj(null); setHasTxChange(true); setHasSigChange(true); - setTxBalances(calculateExhibitionData(txProposal.partialTx, tokensCache, wallet)); + setTxBalances(await calculateExhibitionData(txProposal.partialTx, tokensCache, wallet)); } const handleSendClick = () => { @@ -349,7 +345,7 @@ export default function EditSwap(props) { const signOperationHandler = async ({ pin }) => { const newProposal = PartialTxProposal.fromPartialTx( partialTx.serialize(), - wallet.getNetworkObject() + wallet.storage ); await newProposal.signData(pin); const mySignatures = newProposal.signatures.serialize(); @@ -365,7 +361,7 @@ export default function EditSwap(props) { setHasSigChange(true); // Updating the signed inputs on the partialTx metadata - enrichTxData(newProposal.partialTx, wallet, newSignaturesObj.serialize()); + await enrichTxData(newProposal.partialTx, wallet, newSignaturesObj.serialize()); setPartialTx(newProposal.partialTx); } @@ -375,26 +371,26 @@ export default function EditSwap(props) { }); } - const handleSaveClick = () => { + const handleSaveClick = async () => { // Save to local redux if (hasTxChange) { // Update the selection mark on all old inputs const oldPartialTx = deserializePartialTx(proposal.data.partialTx, wallet); - oldPartialTx.inputs.forEach(old => { - const updatedInput = partialTx.inputs.find(updated => updated.hash === old.hash); + for (const old of oldPartialTx.inputs) { + const updatedInput = partialTx.inputs.find(updated => updated.hash === old.hash); - if (!updatedInput) { - // This input was removed: unmark it - wallet.markUtxoSelected(old.hash, old.index, false); - } - }) + if (!updatedInput) { + // This input was removed: unmark it + await wallet.markUtxoSelected(old.hash, old.index, false); + } + } // Mark all the current inputs as selected - partialTx.inputs.forEach(i => { + for (const i of partialTx.inputs) { if (i.isMine) { - wallet.markUtxoSelected(i.hash, i.index, true); + await wallet.markUtxoSelected(i.hash, i.index, true); } - }) + } // Update the global redux state proposal.data.partialTx = partialTx.serialize(); @@ -425,10 +421,11 @@ export default function EditSwap(props) { modalContext.showModal(MODAL_TYPES.CONFIRM, { title: 'Remove all my inputs and outputs', body: confirmationMessage, - handleYes: () => { + handleYes: async () => { // Unlocking the utxo for this wallet session - const inputsToRemove = partialTx.inputs.filter((input) => input.isMine); - inputsToRemove.forEach(i => wallet.markUtxoSelected(i.hash, i.index, false)); + for (const inputToRemove of partialTx.inputs.filter((input) => input.isMine)) { + await wallet.markUtxoSelected(inputToRemove.hash, inputToRemove.index, false) + } // Removing the inputs from the local state partialTx.inputs = partialTx.inputs.filter(input => { @@ -442,8 +439,8 @@ export default function EditSwap(props) { // Generate a new partialTx and store it const newPartialTx = PartialTx.deserialize(partialTx.serialize(), wallet.getNetworkObject()); - setTxBalances(calculateExhibitionData(newPartialTx, tokensCache, wallet)); - enrichTxData(newPartialTx, wallet); + setTxBalances(await calculateExhibitionData(newPartialTx, tokensCache, wallet)); + await enrichTxData(newPartialTx, wallet); setPartialTx(newPartialTx); setSignaturesObj(null); setHasSigChange(true); @@ -458,6 +455,22 @@ export default function EditSwap(props) { // Effects //------------------------------------------------------- + useEffect(() => { + // We define the async method and call it so we do not use an async method on useEffect + // The effect return should be the cleanup method, but any async will return + // a promise and a Promise would be interpreted as a cleanup funcion, which would + // break when trying to be called. + async function internalEffect() { + const enrichedPartialTx = await deserializePartialTx(proposal.data.partialTx, wallet); + setPartialTx(enrichedPartialTx); + setTxBalances(await calculateExhibitionData(enrichedPartialTx, tokensCache, wallet)); + setSignaturesObj(proposal.data.signatures && proposal.data.signatures.length + ? calculateSignaturesObject(enrichedPartialTx, proposal.data.signatures) + : null); + }; + internalEffect(); + }, []); + // Re-fetching all the tokens involved in every proposal change and calculating ability to sign useEffect(() => { // Getting token uids with an existing method @@ -487,7 +500,7 @@ export default function EditSwap(props) { const fullProposalObj = assembleProposal( partialTx.serialize(), signaturesObj.serialize(), - wallet.getNetworkObject() + wallet.storage ); setShowSendTxButton(fullProposalObj.isComplete()); } @@ -495,7 +508,10 @@ export default function EditSwap(props) { // Enriching local exhibition data with updated token symbol and name useEffect(() => { - setTxBalances(calculateExhibitionData(partialTx, tokensCache, wallet)); + async function internalEffect() { + setTxBalances(await calculateExhibitionData(partialTx, tokensCache, wallet)); + } + internalEffect(); }, [tokensCache]) // Main screen render diff --git a/src/storage.js b/src/storage.js index 7d903392..5bff56f0 100644 --- a/src/storage.js +++ b/src/storage.js @@ -5,9 +5,47 @@ * LICENSE file in the root directory of this source tree. */ -import hathorLib from '@hathor/wallet-lib'; +import CryptoJS from 'crypto-js'; +import { LevelDBStore, Storage, walletUtils, config, network, cryptoUtils, WalletType } from "@hathor/wallet-lib"; +import { VERSION } from "./constants"; + + +export const WALLET_VERSION_KEY = 'localstorage:version'; +// This key holds the storage version to indicate the migration strategy +export const STORE_VERSION_KEY = 'localstorage:storeversion'; +export const LEDGER_APP_VERSION_KEY = 'localstorage:ledger:version'; +// This marks the wallet as being manually locked +export const LOCKED_KEY = 'localstorage:lock'; +// This key marks the wallet as being correctly closed +export const CLOSED_KEY = 'localstorage:closed'; +// This key marks that the user has seen the welcome page and clicked on get started +export const STARTED_KEY = 'localstorage:started'; +export const NETWORK_KEY = 'localstorage:network'; +export const IS_HARDWARE_KEY = 'localstorage:ishardware'; +export const TOKEN_SIGNATURES_KEY = 'localstorage:token:signatures'; +export const IS_BACKUP_DONE_KEY = 'localstorage:backup'; +export const SERVER_KEY = 'localstorage:server'; +export const WS_SERVER_KEY = 'localstorage:wsserver'; + +export const storageKeys = [ + WALLET_VERSION_KEY, + STORE_VERSION_KEY, + LOCKED_KEY, + CLOSED_KEY, + STARTED_KEY, + NETWORK_KEY, + IS_HARDWARE_KEY, + TOKEN_SIGNATURES_KEY, + IS_BACKUP_DONE_KEY, + SERVER_KEY, + WS_SERVER_KEY, +]; + +export class LocalStorageStore { + _storage = null; + + version = 1; -class LocalStorageStore { getItem(key) { let item; try { @@ -39,48 +77,368 @@ class LocalStorageStore { clear() { localStorage.clear(); } -} -/* - * We use this storage so 'wallet:data' is kept in memory. This information may become very large if - * there are thousands of txs on the wallet history (we got this error with 15k txs) and the localStorage - * does not store data after a certain limit (it fails silently). - * - * In theory, there shouldn't be a limit for localStorage with Electron, as it's been patched here: https://github.com/electron/electron/pull/15596. - * However, this other comment (https://github.com/electron/electron/issues/13465#issuecomment-494983533) suggests - * there was still a problem. - * - * It's not a problem if 'wallet:data' is not persisted, as this data is always loaded when we connect to the server. - */ -class HybridStore { - constructor() { - this.memStore = new hathorLib.MemoryStore(); - this.persistentStore = new LocalStorageStore(); + getWalletId() { + return this.getItem('wallet:id'); + } + + setWalletId(walletId) { + this.setItem('wallet:id', walletId); + } + + cleanWallet() { + this.removeItem('wallet:id'); + this.removeItem(IS_HARDWARE_KEY); + this.removeItem(STARTED_KEY); + this.removeItem(LOCKED_KEY); + this.removeItem(CLOSED_KEY); + delete this._storage; + this._storage = null; } - _getStore(key) { - if (key === 'wallet:data') { - return this.memStore; + resetStorage() { + this.removeItem('wallet:id'); + + for (const key of storageKeys) { + this.removeItem(key); } - return this.persistentStore; } - getItem(key) { - return this._getStore(key).getItem(key); + async initStorage(seed, password, pin) { + this._storage = null; + this.setHardwareWallet(false); + const accessData = walletUtils.generateAccessDataFromSeed( + seed, + { + pin, + password, + networkName: config.getNetwork().name, + } + ); + const walletId = walletUtils.getWalletIdFromXPub(accessData.xpubkey); + this.setWalletId(walletId); + const storage = this.getStorage(); + await storage.saveAccessData(accessData); + this._storage = storage; + this.updateStorageVersion(); + return storage; } - setItem(key, value) { - return this._getStore(key).setItem(key, value); + async initHWStorage(xpub) { + this._storage = null; + this.setHardwareWallet(true); + const accessData = walletUtils.generateAccessDataFromXpub( + xpub, + { hardware: true } + ); + const walletId = walletUtils.getWalletIdFromXPub(accessData.xpubkey); + this.setWalletId(walletId); + const storage = this.getStorage(); + await storage.saveAccessData(accessData); + this._storage = storage; + return storage; } - removeItem(key) { - return this._getStore(key).removeItem(key); + /** + * Get a Storage instance for the loaded wallet. + * @returns {Storage|null} Storage instance if the wallet is loaded. + */ + getStorage() { + if (!this._storage) { + const walletId = this.getWalletId(); + if (!walletId) { + return null; + } + + const store = new LevelDBStore(walletId); + this._storage = new Storage(store); + } + return this._storage; } - clear() { - this.memStore.clear(); - this.persistentStore.clear(); + /** + * Get access data of loaded wallet from async storage. + * + * @returns {Promise} + */ + async _getAccessData() { + const storage = this.getStorage(); + if (!storage) { + return null; + } + return storage.getAccessData(); + } + + /** + * Will attempt to load the access data from either the old or new storage. + * This will return the access data as it was found, so the format will be different. + * To check which format was received, use the storage version that is returned. + * + * @returns {Promise<{ + * accessData: IWalletAccessData|null, + * version: number, + * }>} The access data and the storage version. + */ + async getAvailableAccessData() { + // First we try to fetch the old access data (if we haven't migrated yet) + let accessData = this.getItem('wallet:accessData'); + if (!accessData) { + // If we don't have the old access data, we try to fetch the new one + accessData = await this._getAccessData(); + } + + return accessData; + } + + /** + * Check if the wallet is loaded. + * Only works after preload is called and hathorMemoryStorage is populated. + * + * @returns {Promise} Whether we have a loaded wallet on the storage. + */ + async isLoaded() { + const accessData = await this.getAvailableAccessData(); + return !!accessData; + } + + /** + * Get the storage version. + * @returns {number|null} Storage version if it exists on AsyncStorage. + */ + getStorageVersion() { + return this.getItem(STORE_VERSION_KEY); + } + + /** + * Update the storage version to the most recent one. + */ + updateStorageVersion() { + this.setItem(STORE_VERSION_KEY, this.version); + } + + getOldWalletWords(password) { + const accessData = this.getItem('wallet:accessData'); + if (!accessData) { + return null; + } + + const decryptedWords = CryptoJS.AES.decrypt(accessData.words, password); + return decryptedWords.toString(CryptoJS.enc.Utf8); + } + + /** + * Migrate registered tokens from the old storage into the new storage + * The old storage holds an array of token data and the new storage expects + * an object with the key as uid and value as token data. + * + * @async + */ + async handleMigrationOldRegisteredTokens(storage) { + const oldTokens = this.getItem('wallet:tokens'); + for (const token of oldTokens) { + await storage.registerToken(token); + } + } + + /** + * Handle data migration from old versions of the wallet to the most recent and usable + * + * @param {String} pin Unlock PIN written by the user + * @async + */ + async handleDataMigration(pin) { + const storageVersion = this.getStorageVersion(); + if (storageVersion === null) { + // We are migrating from an version of wallet-lib prior to 1.0.0 + // This will generate the encrypted keys and other metadata + const accessData = this.migrateAccessData(pin); + // Prepare the storage with the migrated access data + this._storage = null; + this.setHardwareWallet(false); + const walletId = walletUtils.getWalletIdFromXPub(accessData.xpubkey); + this.setWalletId(walletId); + const storage = this.getStorage(); + await storage.saveAccessData(accessData); + this._storage = storage; + + await this.handleMigrationOldRegisteredTokens(storage); + const isBackupDone = this.getItem('wallet:backup'); + if (isBackupDone) { + this.markBackupDone(); + } + + // The access data is saved on the new storage, we can delete the old data. + // This will only delete keys with the wallet prefix, so we don't delete + // the biometry keys and new data. + await this.clearItems(true); + } + // We have finished the migration so we can set the storage version to the most recent one. + this.updateStorageVersion(); + } + + migrateAccessData(pin) { + const oldAccessData = this.getItem('wallet:accessData'); + let acctPathKey; + let authKey; + if (oldAccessData.acctPathMainKey) { + const decryptedAcctKey = CryptoJS.AES.decrypt(oldAccessData.acctPathMainKey, pin); + const acctKeyStr = decryptedAcctKey.toString(CryptoJS.enc.Utf8); + acctPathKey = cryptoUtils.encryptData(acctKeyStr, pin); + } + if (oldAccessData.authKey) { + const decryptedAuthKey = CryptoJS.AES.decrypt(oldAccessData.authKey, pin); + const authKeyStr = decryptedAuthKey.toString(CryptoJS.enc.Utf8); + authKey = cryptoUtils.encryptData(authKeyStr, pin); + } + + return { + walletType: WalletType.P2PKH, + walletFlags: 0, + xpubkey: oldAccessData.xpubkey, + acctPathKey, + authKey, + words: { + data: oldAccessData.words, + hash: oldAccessData.hashPasswd, + salt: oldAccessData.saltPasswd, + iterations: oldAccessData.hashIterations, + pbkdf2Hasher: oldAccessData.pbkdf2Hasher, + }, + mainKey: { + data: oldAccessData.mainKey, + hash: oldAccessData.hash, + salt: oldAccessData.salt, + iterations: oldAccessData.hashIterations, + pbkdf2Hasher: oldAccessData.pbkdf2Hasher, + }, + }; + } + + async checkPin(pinCode) { + const accessData = await this.getAvailableAccessData(); + let mainEncryptedData = accessData.mainKey; + if (!mainEncryptedData.data) { + // Old storage + mainEncryptedData = { + data: accessData.mainKey, + hash: accessData.hash, + salt: accessData.salt, + iterations: accessData.hashIterations, + pbkdf2Hasher: accessData.pbkdf2Hasher, + }; + } + + return cryptoUtils.checkPassword(mainEncryptedData, pinCode); + } + + /** + * Persist server URLs on the localStorage. + * @param {string} serverURL Fullnode api url + * @param {string} wsServerURL websocket server url for wallet-service + */ + setServers(serverURL, wsServerURL) { + this.setItem(SERVER_KEY, serverURL); + if (wsServerURL) { + this.setItem(WS_SERVER_KEY, serverURL); + } + } + + getServer() { + return this.getItem(SERVER_KEY); + } + + getWsServer() { + return this.getItem(WS_SERVER_KEY); + } + + lock() { + this.setItem(LOCKED_KEY, true); + } + + unlock() { + this.setItem(LOCKED_KEY, false); + } + + isLocked() { + return this.getItem(LOCKED_KEY) ?? true; + } + + close() { + this.setItem(CLOSED_KEY, true); + } + + wasClosed() { + return this.getItem(CLOSED_KEY) || false; + } + + open() { + this.setItem(CLOSED_KEY, false); + } + + wasStarted() { + return this.getItem(STARTED_KEY); + } + + markWalletAsStarted() { + this.setItem(STARTED_KEY, true); + } + + getWalletVersion() { + return this.getItem(WALLET_VERSION_KEY); + } + + setWalletVersion() { + this.setItem(WALLET_VERSION_KEY, VERSION); + } + + setNetwork(networkName) { + this.setItem(NETWORK_KEY, networkName); + network.setNetwork(networkName); + } + + getNetwork() { + return this.getItem(NETWORK_KEY); + } + + setHardwareWallet(value) { + this.setItem(IS_HARDWARE_KEY, value); + } + + isHardwareWallet() { + return this.getItem(IS_HARDWARE_KEY) || false; + } + + getTokenSignatures() { + return this.getItem(TOKEN_SIGNATURES_KEY) || {}; + } + + setTokenSignatures(data) { + this.setItem(TOKEN_SIGNATURES_KEY, data); + } + + resetTokenSignatures() { + this.removeItem(TOKEN_SIGNATURES_KEY); + } + + markBackupDone() { + this.setItem(IS_BACKUP_DONE_KEY, true); + } + + markBackupAsNotDone() { + this.removeItem(IS_BACKUP_DONE_KEY); + } + + isBackupDone() { + return !!this.getItem(IS_BACKUP_DONE_KEY) + } + + saveLedgerAppVersion(version) { + this.setItem(LEDGER_APP_VERSION_KEY, version); + } + + getLedgerAppVersion() { + return this.getItem(LEDGER_APP_VERSION_KEY); } } -export { LocalStorageStore, HybridStore }; +export default new LocalStorageStore(); diff --git a/src/storageInstance.js b/src/storageInstance.js deleted file mode 100644 index d7703a32..00000000 --- a/src/storageInstance.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * copyright (c) hathor labs and its affiliates. - * - * this source code is licensed under the mit license found in the - * license file in the root directory of this source tree. - */ - -import { HybridStore } from './storage.js'; - -/** - * Store to be used in the wallet and all HathorWallet objects created - */ -const STORE = new HybridStore(); - -export default STORE; diff --git a/src/utils/atomicSwap.js b/src/utils/atomicSwap.js index 8ac4d06b..7dce7009 100644 --- a/src/utils/atomicSwap.js +++ b/src/utils/atomicSwap.js @@ -5,12 +5,18 @@ * LICENSE file in the root directory of this source tree. */ -import hathorLib, { PartialTx, PartialTxInputData, PartialTxProposal, storage } from "@hathor/wallet-lib"; -import { HATHOR_TOKEN_CONFIG, TOKEN_MELT_MASK, TOKEN_MINT_MASK } from "@hathor/wallet-lib/lib/constants"; +import { + transactionUtils, + PartialTx, + PartialTxInputData, + PartialTxProposal, + tokensUtils, + config as hathorLibConfig, +} from "@hathor/wallet-lib"; +import { TOKEN_MINT_MASK, TOKEN_MELT_MASK, HATHOR_TOKEN_CONFIG } from "@hathor/wallet-lib/lib/constants"; import { get } from 'lodash'; import walletUtil from "./wallet"; -const { wallet: oldWallet, config: hathorLibConfig } = hathorLib; /** * @typedef ProposalData * @property {string} id Proposal identifier @@ -86,12 +92,12 @@ export function generateEmptyProposal(wallet) { * @param {HathorWallet} wallet * @param {string} [strSignatures] Optional signatures string */ -export function enrichTxData(partialTx, wallet, strSignatures) { +export async function enrichTxData(partialTx, wallet, strSignatures) { const signaturesObj = strSignatures && calculateSignaturesObject(partialTx, strSignatures); for (const inputIndex in partialTx.inputs) { const input = partialTx.inputs[inputIndex]; - if (wallet.isAddressMine(input.address)) { + if (await wallet.isAddressMine(input.address)) { input.isMine = true; input.indexOnTx = +inputIndex; } @@ -103,7 +109,7 @@ export function enrichTxData(partialTx, wallet, strSignatures) { const output = partialTx.outputs[outputIndex]; output.parseScript(wallet.getNetworkObject()); output.address = get(output,'decodedScript.address.base58','Unknown Address'); - if (wallet.isAddressMine(output.address)) { + if (await wallet.isAddressMine(output.address)) { output.isMine = true; output.indexOnTx = +outputIndex; } @@ -123,9 +129,9 @@ export function enrichTxData(partialTx, wallet, strSignatures) { * @param {PartialTx} partialTx * @param {Record} cachedTokens * @param {HathorWallet} wallet - * @returns {DisplayBalance[]} + * @returns {Promise} */ -export function calculateExhibitionData(partialTx, cachedTokens, wallet) { +export async function calculateExhibitionData(partialTx, cachedTokens, wallet) { const getTokenOrCreate = (tokenUid) => { const cachedToken = cachedTokens[tokenUid]; if (!cachedToken) { @@ -140,8 +146,8 @@ export function calculateExhibitionData(partialTx, cachedTokens, wallet) { return cachedTokens[tokenUid]; } - const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), wallet.getNetworkObject()); - const balance = txProposal.calculateBalance(wallet); + const txProposal = PartialTxProposal.fromPartialTx(partialTx.serialize(), wallet.storage); + const balance = await txProposal.calculateBalance(); // Calculating the difference between them both const balanceArr = []; @@ -224,10 +230,10 @@ export function canISign(partialTx, currentSignatures) { return false; } -export function deserializePartialTx(strPartialTx, wallet) { +export async function deserializePartialTx(strPartialTx, wallet) { const networkObject = wallet.getNetworkObject(); const partialTx = PartialTx.deserialize(strPartialTx, networkObject); - enrichTxData(partialTx, wallet); + await enrichTxData(partialTx, wallet); return partialTx; } @@ -236,11 +242,11 @@ export function deserializePartialTx(strPartialTx, wallet) { * Assemble a transaction from the serialized partial tx and signatures * @param {string} partialTx The serialized partial tx * @param {string} signatures The serialized signatures - * @param {Network} network The network object + * @param {IStorage} storage The storage object * @see https://github.com/HathorNetwork/hathor-wallet-headless/blob/fd1fb5d9757871bdf367e0496cfa85be8175e09d/src/services/atomic-swap.service.js */ -export const assembleProposal = (partialTx, signatures, network) => { - const proposal = PartialTxProposal.fromPartialTx(partialTx, network); +export const assembleProposal = (partialTx, signatures, storage) => { + const proposal = PartialTxProposal.fromPartialTx(partialTx, storage); const tx = proposal.partialTx.getTx(); const inputData = new PartialTxInputData(tx.getDataToSign().toString('hex'), tx.inputs.length); @@ -273,48 +279,48 @@ export const assembleProposal = (partialTx, signatures, network) => { * @see https://github.com/HathorNetwork/hathor-wallet-headless/blob/fd1fb5d9757871bdf367e0496cfa85be8175e09d/src/controllers/wallet/atomic-swap/tx-proposal.controller.js#L56-L97 * @returns {null | ProposalCustomUtxo} */ -export const translateTxToProposalUtxo = (txId, index, wallet) => { - const txData = wallet.getTx(txId); - if (!txData) { - // utxo not in history - return null; - } - const txout = txData.outputs[index]; - if (!oldWallet.canUseUnspentTx(txout, txData.height)) { - // Cannot use this utxo - return null; - } - - const addressIndex = wallet.getAddressIndex(txout.decoded.address); - const addressPath = addressIndex ? wallet.getAddressPathForIndex(addressIndex) : ''; - let authorities = 0; - if (oldWallet.isMintOutput(txout)) { - authorities += TOKEN_MINT_MASK; - } - if (oldWallet.isMeltOutput(txout)) { - authorities += TOKEN_MELT_MASK; - } - - let tokenId = txout.token; - if (!tokenId) { - const tokenIndex = oldWallet.getTokenIndex(txout.token_data) - 1; - tokenId = txout.token_data === 0 - ? HATHOR_TOKEN_CONFIG.uid - : txData.tx.tokens[tokenIndex].uid; - } - - return { - txId: txId, - index: index, - value: txout.value, - address: txout.decoded.address, - timelock: txout.decoded.timelock, - tokenId, - authorities, - addressPath, - heightlock: null, - locked: false, - }; +export const translateTxToProposalUtxo = async (txId, index, wallet) => { + const utxo = { txId, index }; + if (!await transactionUtils.canUseUtxo(utxo, wallet.storage)) { + // Cannot use this utxo + return null; + } + + const tx = await wallet.getTx(txId); + const txout = tx.outputs[index]; + // We use the storage getAddressInfo because the wallet.getAddressInfo + // has some unnecessary calculations + const addressInfo = await wallet.storage.getAddressInfo(txout.decoded.address); + const addressIndex = addressInfo.bip32AddressIndex; + + const addressPath = addressIndex ? await wallet.getAddressPathForIndex(addressIndex) : ''; + let authorities = 0; + if (transactionUtils.isMint(txout)) { + authorities += TOKEN_MINT_MASK; + } + if (transactionUtils.isMelt(txout)) { + authorities += TOKEN_MELT_MASK; + } + + let tokenId = txout.token; + if (!tokenId) { + // This should never happen since the fullnode always sends the token uid. + const tokenIndex = tokensUtils.getTokenIndexFromData(txout.token_data); + tokenId = [HATHOR_TOKEN_CONFIG.uid, ...tx.tokens][tokenIndex]; + } + + return { + txId: txId, + index: index, + value: txout.value, + address: txout.decoded.address, + timelock: txout.decoded.timelock, + tokenId, + authorities, + addressPath, + heightlock: null, + locked: false, + }; } /** @@ -360,7 +366,7 @@ export const generateReduxObjFromProposal = (proposalId, password, partialTx, wa updatedAt: new Date().valueOf(), }; // Building the object to retrieve data from - const txProposal = new PartialTxProposal.fromPartialTx(partialTx, wallet.getNetworkObject()); + const txProposal = PartialTxProposal.fromPartialTx(partialTx, wallet.storage); // Calculating the amount of tokens const balance = txProposal.calculateBalance(wallet); @@ -398,7 +404,7 @@ export const generateReduxObjFromProposal = (proposalId, password, partialTx, wa export function initializeSwapServiceBaseUrlForWallet(network) { // XXX: This storage item is currently unchangeable via the wallet UI, and is available // only for debugging purposes on networks other than mainnet and testnet - const configUrl = storage.getItem('wallet:atomic_swap_service:base_server') + const configUrl = localStorage.getItem('wallet:atomic_swap_service:base_server') // Configures Atomic Swap Service url. Prefers explicit config input, then network-based if (configUrl) { hathorLibConfig.setSwapServiceBaseUrl(configUrl); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index fb49b8f6..93e2a65a 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -11,6 +11,7 @@ import { get } from 'lodash'; import store from '../store/index'; import { networkUpdate } from '../actions/index'; import { EXPLORER_BASE_URL, TESTNET_EXPLORER_BASE_URL } from '../constants'; +import LOCAL_STORE from '../storage'; let shell = null; if (window.require) { @@ -50,8 +51,8 @@ const helpers = { updateNetwork(network) { // Update network in redux store.dispatch(networkUpdate({network})); - hathorLib.storage.setItem('wallet:network', network); hathorLib.network.setNetwork(network); + LOCAL_STORE.setNetwork(network); }, /** @@ -64,7 +65,7 @@ const helpers = { * @inner */ getExplorerURL() { - const currentNetwork = hathorLib.storage.getItem('wallet:network') || 'mainnet'; + const currentNetwork = LOCAL_STORE.getNetwork() || 'mainnet'; if (currentNetwork === 'mainnet') { return EXPLORER_BASE_URL; } else { @@ -84,9 +85,9 @@ const helpers = { */ renderValue(amount, isInteger) { if (isInteger) { - return hathorLib.helpersUtils.prettyIntegerValue(amount); + return hathorLib.numberUtils.prettyIntegerValue(amount); } else { - return hathorLib.helpersUtils.prettyValue(amount); + return hathorLib.numberUtils.prettyValue(amount); } }, @@ -254,6 +255,17 @@ const helpers = { ms ) }) + }, + + /** + * Return either the single or plural form depending on the quantity. + * @param {number} qty Quantity to access + * @param {string} singleWord word in single format + * @param {string} pluralWord word in plural format + * @returns {string} + */ + plural(qty, singleWord, pluralWord) { + return qty === 1 ? singleWord : pluralWord; } } diff --git a/src/utils/ledger.js b/src/utils/ledger.js index 8fe3dea9..a8bf4f0c 100644 --- a/src/utils/ledger.js +++ b/src/utils/ledger.js @@ -52,14 +52,14 @@ export const serializeTokenInfo = (token, hasSignature) => { const arr = []; // 0: token version = 1 (always) - arr.push(hathorLib.helpersUtils.intToBytes(LEDGER_TOKEN_VERSION, 1)); + arr.push(hathorLib.bufferUtils.intToBytes(LEDGER_TOKEN_VERSION, 1)); // 1: uid bytes (length is fixed 32 bytes) arr.push(uidBytes); // 2, 3: symbol length + bytes - arr.push(hathorLib.helpersUtils.intToBytes(symbolBytes.length, 1)); + arr.push(hathorLib.bufferUtils.intToBytes(symbolBytes.length, 1)); arr.push(symbolBytes); // 4, 5: name length + bytes - arr.push(hathorLib.helpersUtils.intToBytes(nameBytes.length, 1)); + arr.push(hathorLib.bufferUtils.intToBytes(nameBytes.length, 1)); arr.push(nameBytes); if (hasSignature) { @@ -118,11 +118,12 @@ if (IPC_RENDERER) { * @param {Object} data Transaction data * @param {number} changeIndex Index of the change output. -1 in case there is no change * @param {number} changeKeyIndex Index of address of the change output + * @param {Network} network The network of the configured wallet, so we generate the data correctly * * @memberof Ledger * @inner */ - sendTx(data, changeInfo, useOldProtocol) { + sendTx(data, changeInfo, useOldProtocol, network) { // XXX: if custom tokens not allowed, use old protocol for first change output // first assemble data to be sent const arr = []; @@ -133,14 +134,14 @@ if (IPC_RENDERER) { const change = changeInfo[0]; const changeBuffer = formatPathData(change.keyIndex) // encode the bit indicating existence of change and change path length on first byte - arr.push(hathorLib.transaction.intToBytes(0x80 | changeBuffer[0], 1)) + arr.push(hathorLib.bufferUtils.intToBytes(0x80 | changeBuffer[0], 1)) // change output index on the second - arr.push(hathorLib.transaction.intToBytes(change.outputIndex, 1)) + arr.push(hathorLib.bufferUtils.intToBytes(change.outputIndex, 1)) // Change key path of the address arr.push(changeBuffer.slice(1)); } else { // no change output - arr.push(hathorLib.transaction.intToBytes(0, 1)); + arr.push(hathorLib.bufferUtils.intToBytes(0, 1)); } } else { // Protocol v1 @@ -150,14 +151,15 @@ if (IPC_RENDERER) { // - 1 byte for output index // - bip32 path (can be up to 21 bytes) arr.push(Buffer.from([0x01])); - arr.push(hathorLib.transaction.intToBytes(changeInfo.length, 1)); + arr.push(hathorLib.bufferUtils.intToBytes(changeInfo.length, 1)); changeInfo.forEach(change => { - arr.push(hathorLib.transaction.intToBytes(change.outputIndex, 1)); + arr.push(hathorLib.bufferUtils.intToBytes(change.outputIndex, 1)); arr.push(formatPathData(change.keyIndex)); }); } const initialData = Buffer.concat(arr); - const dataBytes = hathorLib.transaction.dataToSign(data); + const tx = hathorLib.transactionUtils.createTransactionFromData(data, network); + const dataBytes = tx.getDataToSign(); const dataToSend = Buffer.concat([initialData, dataBytes]); IPC_RENDERER.send("ledger:sendTx", dataToSend); @@ -167,16 +169,17 @@ if (IPC_RENDERER) { * Get tx signatures from ledger * * @param {Object} data Transaction data + * @param {HathorWallet} wallet Wallet to get address indexes * * @memberof Ledger * @inner */ - getSignatures(data, keys) { + async getSignatures(data, wallet) { // send key indexes as 4-byte integers const arr = []; for (const input of data.inputs) { - const index = keys[input.address].index; - arr.push(index); + const addressIndex = await wallet.getAddressIndex(input.address); + arr.push(addressIndex); } IPC_RENDERER.send("ledger:getSignatures", arr); }, diff --git a/src/utils/tokens.js b/src/utils/tokens.js index 51a25765..82b850b8 100644 --- a/src/utils/tokens.js +++ b/src/utils/tokens.js @@ -9,7 +9,7 @@ import store from '../store/index'; import { newTokens, removeTokenMetadata } from '../actions/index'; import wallet from './wallet'; import hathorLib from '@hathor/wallet-lib'; - +import LOCAL_STORE from '../storage'; /** * Methods to create and handle tokens @@ -18,6 +18,33 @@ import hathorLib from '@hathor/wallet-lib'; */ const tokens = { + /** + * Get registered tokens from the wallet instance. + * @param {HathorWallet} wallet + * @param {boolean} excludeDefaultToken If we should exclude the default token. + * @returns {Promise} + */ + async getRegisteredTokens(wallet, excludeDefaultToken = false) { + const htrUid = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; + const tokens = []; + + // redux-saga generator magic does not work well with the "for await..of" syntax + // The asyncGenerator is not recognized as an iterable and it throws an exception + // So we must iterate manually, awaiting each "next" call + const iterator = wallet.storage.getRegisteredTokens(); + let next = await iterator.next(); + while (!next.done) { + const token = next.value; + if ((!excludeDefaultToken) || token.uid !== htrUid) { + tokens.push({ uid: token.uid, name: token.name, symbol: token.symbol }); + } + // eslint-disable-next-line no-await-in-loop + next = await iterator.next(); + } + + return tokens; + }, + /** * Add a new token to the localStorage and redux * @@ -28,32 +55,15 @@ const tokens = { * @memberof Tokens * @inner */ - addToken(uid, name, symbol) { - const tokens = hathorLib.tokens.addToken(uid, name, symbol); - store.dispatch(newTokens({tokens, uid: uid})); + async addToken(uid, name, symbol) { const reduxState = store.getState(); const reduxWallet = reduxState.wallet; + await reduxWallet.storage.registerToken({ uid, name, symbol }); + const tokens = await this.getRegisteredTokens(reduxWallet); + store.dispatch(newTokens({tokens, uid: uid})); wallet.fetchTokensMetadata([uid], reduxWallet.conn.network); }, - /** - * Edit token name and symbol. Save in localStorage and redux - * - * @param {string} uid Token uid to be edited - * @param {string} name New token name - * @param {string} synbol New token symbol - * - * @return {Object} edited token - * - * @memberof Tokens - * @inner - */ - editToken(uid, name, symbol) { - const tokens = hathorLib.tokens.editToken(uid, name, symbol); - store.dispatch(newTokens({tokens, uid})); - return {uid, name, symbol}; - }, - /** * Unregister token from localStorage and redux * @@ -64,46 +74,29 @@ const tokens = { * @memberof Tokens * @inner */ - unregisterToken(uid) { - const promise = new Promise((resolve, reject) => { - const libPromise = hathorLib.tokens.unregisterToken(uid); - libPromise.then((tokens) => { - store.dispatch(newTokens({tokens, uid: hathorLib.constants.HATHOR_TOKEN_CONFIG.uid})); - store.dispatch(removeTokenMetadata(uid)); - resolve(); - }, (e) => { - reject(e); - }); - }); - return promise; - }, - - /** - * Save new tokens array and selected token after a new one - * - * @param {string} uid Token uid added - * - * @memberof Tokens - * @inner - */ - saveTokenRedux(uid) { - const storageTokens = hathorLib.storage.getItem('wallet:tokens'); - store.dispatch(newTokens({tokens: storageTokens, uid: uid})); + async unregisterToken(uid) { + const reduxState = store.getState(); + const reduxWallet = reduxState.wallet; + await reduxWallet.storage.unregisterToken(uid); + const tokens = await this.getRegisteredTokens(reduxWallet); + store.dispatch(newTokens({tokens, uid: hathorLib.constants.HATHOR_TOKEN_CONFIG.uid})); + store.dispatch(removeTokenMetadata(uid)); }, /** * Returns the deposit amount in 'pretty' format * * @param {number} mintAmount Amount of tokens to mint + * @param {number} depositPercent deposit percentage for creating tokens * * @memberof Tokens * @inner */ - getDepositAmount(mintAmount) { + getDepositAmount(mintAmount, depositPercent) { if (mintAmount) { const amountValue = wallet.decimalToInteger(mintAmount); - const deposit = hathorLib.tokens.getDepositAmount(amountValue); - return hathorLib.helpers.prettyValue(deposit); + const deposit = hathorLib.tokensUtils.getDepositAmount(amountValue, depositPercent); + return hathorLib.numberUtils.prettyValue(deposit); } else { return 0; } @@ -129,9 +122,7 @@ const tokens = { * @inner */ getTokenSignatures() { - const tokenSignatures = hathorLib.storage.getItem('wallet:token:signatures'); - if (!tokenSignatures) return {}; - return tokenSignatures; + return LOCAL_STORE.getTokenSignatures(); }, /** @@ -162,7 +153,7 @@ const tokens = { addTokenSignature(uid, signature) { const tokenSignatures = this.getTokenSignatures(); tokenSignatures[uid] = signature; - hathorLib.storage.setItem('wallet:token:signatures', tokenSignatures); + LOCAL_STORE.setTokenSignatures(tokenSignatures); }, /** @@ -173,7 +164,7 @@ const tokens = { * @inner */ resetTokenSignatures() { - hathorLib.storage.setItem('wallet:token:signatures', {}); + LOCAL_STORE.resetTokenSignatures(); }, /** @@ -188,7 +179,7 @@ const tokens = { removeTokenSignature(uid) { const tokenSignatures = this.getTokenSignatures(); delete tokenSignatures[uid]; - hathorLib.storage.setItem('wallet:token:signatures', tokenSignatures); + LOCAL_STORE.setTokenSignatures(tokenSignatures); }, } diff --git a/src/utils/version.js b/src/utils/version.js index 317adc09..9d70f2fc 100644 --- a/src/utils/version.js +++ b/src/utils/version.js @@ -10,6 +10,7 @@ import { isVersionAllowedUpdate } from '../actions/index'; import { FIRST_WALLET_COMPATIBLE_VERSION, LEDGER_FIRST_CUSTOM_TOKEN_COMPATIBLE_VERSION } from '../constants'; import helpers from './helpers'; import hathorLib from '@hathor/wallet-lib'; +import LOCAL_STORE from '../storage'; /** * Methods to validate version @@ -28,6 +29,9 @@ const version = { * @inner */ async checkApiVersion(wallet) { + if (!wallet) { + return; + } const data = await wallet.getVersionData(); /** @@ -35,7 +39,7 @@ const version = { * is allowed by checking it against the MIN_API_VERSION constant from the library. */ store.dispatch(isVersionAllowedUpdate({ - allowed: hathorLib.helpers.isVersionAllowed( + allowed: hathorLib.helpersUtils.isVersionAllowed( data.version, hathorLib.constants.MIN_API_VERSION ), @@ -64,12 +68,12 @@ const version = { * @inner */ checkWalletVersion() { - const version = hathorLib.storage.getItem('wallet:version'); - if (version !== null && hathorLib.helpers.isVersionAllowed(version, FIRST_WALLET_COMPATIBLE_VERSION)) { + const version = LOCAL_STORE.getWalletVersion(); + if (version === null) { + // We do not have a version to check yet, so we will let this check pass. return true; - } else { - return false; } + return hathorLib.helpersUtils.isVersionAllowed(version, FIRST_WALLET_COMPATIBLE_VERSION); }, /** @@ -79,7 +83,7 @@ const version = { * @inner */ isLedgerCustomTokenAllowed() { - const version = hathorLib.storage.getItem('ledger:version'); + const version = LOCAL_STORE.getWalletVersion(); if (version !== null) return helpers.cmpVersionString(version, LEDGER_FIRST_CUSTOM_TOKEN_COMPATIBLE_VERSION) >= 0; return false; } diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 8718ea5f..331d040e 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -7,10 +7,8 @@ import { SENTRY_DSN, - DEBUG_LOCAL_DATA_KEYS, WALLET_HISTORY_COUNT, METADATA_CONCURRENT_DOWNLOAD, - IGNORE_WS_TOGGLE_FLAG, } from '../constants'; import store from '../store/index'; import { @@ -24,16 +22,17 @@ import { resetSelectedTokenIfNeeded, } from '../actions/index'; import { + Address, constants as hathorConstants, errors as hathorErrors, - wallet as oldWalletUtil, walletUtils, - storage, - tokens, metadataApi, + storageUtils, + Network, } from '@hathor/wallet-lib'; import { chunk, get } from 'lodash'; import helpers from '../utils/helpers'; +import LOCAL_STORE from '../storage'; let Sentry = null; // Need to import with window.require in electron (https://github.com/electron/electron/issues/7300) @@ -97,6 +96,25 @@ const storageKeys = { * @namespace Wallet */ const wallet = { + /** + * Validates an address + * + * @param {string} address Address in base58 + * + * @return {boolean} boolean indicating if address is valid + */ + validateAddress(address) { + const networkName = LOCAL_STORE.getNetwork() || 'mainnet'; + const networkObj = new Network(networkName); + try { + const addressObj = new Address(address, { network: networkObj }); + addressObj.validateAddress(); + return true; + } catch (e) { + return false; + } + }, + /** * Validate if can generate the wallet with those parameters and then, call to generate it * @@ -130,40 +148,12 @@ const wallet = { })) }, - /** - * Get all tokens that this wallet has any transaction and fetch balance/history for each of them - * We could do a lazy history load only when the user selects to see the token - * but this would change the behaviour of the wallet and was not the goal of this moment - * We should do this in the future anwyay - * - * wallet {HathorWallet} wallet object - */ - async fetchWalletData(wallet) { - // First we get the tokens in the wallet - const tokens = await wallet.getTokens(); - - const tokensHistory = {}; - const tokensBalance = {}; - // Then for each token we get the balance and history - for (const token of tokens) { - /* eslint-disable no-await-in-loop */ - // We fetch history count of 5 pages and then we fetch a new page each 'Next' button clicked - const history = await wallet.getTxHistory({ token_id: token, count: 5 * WALLET_HISTORY_COUNT }); - tokensBalance[token] = await this.fetchTokenBalance(wallet, token); - tokensHistory[token] = history.map((element) => helpers.mapTokenHistory(element, token)); - /* eslint-enable no-await-in-loop */ - } - - // Then we get from the addresses iterator all addresses - return { tokensHistory, tokensBalance, tokens }; - }, - /** * Fetch paginated history for specific token * - * wallet {HathorWallet} wallet object - * token {string} Token uid - * history {Array} current token history array + * @param {HathorWallet} wallet wallet instance + * @param {string} token Token uid + * @param {Array} history current token history array */ async fetchMoreHistory(wallet, token, history) { const newHistory = await wallet.getTxHistory({ token_id: token, skip: history.length, count: WALLET_HISTORY_COUNT }); @@ -180,8 +170,8 @@ const wallet = { * Method that fetches the balance of a token * and pre process for the expected format * - * wallet {HathorWallet} wallet object - * uid {String} Token uid to fetch balance + * @param {HathorWallet} wallet wallet instance + * @param {String} uid Token uid to fetch balance */ async fetchTokenBalance(wallet, uid) { const balance = await wallet.getBalance(uid); @@ -211,12 +201,11 @@ const wallet = { * * @param {Array} tokens Array of token uids * @param {String} network Network name - * @param {Number} downloadRetry Number of retries already done * * @memberof Wallet * @inner **/ - async fetchTokensMetadata(tokens, network, downloadRetry = 0) { + async fetchTokensMetadata(tokens, network) { const metadataPerToken = {}; const errors = []; @@ -270,7 +259,7 @@ const wallet = { * @param {Object[]} registeredTokens list of registered tokens * @param {Object[]} tokensBalance data about token balances * @param {boolean} hideZeroBalance If true, omits tokens with zero balance - * @returns {{uid:string, balance:{available:number,locked:number}}[]} + * @returns {Promise<{uid:string, balance:{available:number,locked:number}}[]>} */ fetchUnknownTokens(allTokens, registeredTokens, tokensBalance, hideZeroBalance) { const alwaysShowTokensArray = this.listTokensAlwaysShow(); @@ -347,7 +336,7 @@ const wallet = { const totalBalance = balance.available + balance.locked; // This token has zero balance: skip it. - if (hideZeroBalance && totalBalance === 0) { + if (totalBalance === 0) { continue; } } @@ -358,52 +347,63 @@ const wallet = { return filteredTokens; }, + /** + * + * @param {HathorWallet} wallet The wallet instance + * @param {string} pin The pin entered by the user + * @param {any} routerHistory + */ async changeServer(wallet, pin, routerHistory) { - wallet.stop({ cleanStorage: false }); + await wallet.stop({ cleanStorage: false }); + + const isHardwareWallet = await wallet.isHardwareWallet(); - if (oldWalletUtil.isSoftwareWallet()) { + // XXX: check if we would require the seed or xpriv to start the wallet + if (!isHardwareWallet) { store.dispatch(startWalletRequested({ passphrase: '', pin, password: '', routerHistory, - fromXpriv: true, + hardware: false, })); } else { store.dispatch(startWalletRequested({ passphrase: '', password: '', routerHistory, - fromXpriv: false, xpub: wallet.xpub, + hardware: true, })); } }, - /* + /** * After load address history we should update redux data * + * @param {HathorWallet} wallet The wallet instance + * * @memberof Wallet * @inner */ - afterLoadAddressHistory() { + async afterLoadAddressHistory(wallet) { + // runs after load address history store.dispatch(loadingAddresses(false)); - const data = oldWalletUtil.getWalletData(); - // Update historyTransactions with new one - const historyTransactions = 'historyTransactions' in data ? data['historyTransactions'] : {}; - const allTokens = 'allTokens' in data ? data['allTokens'] : []; - const transactionsFound = Object.keys(historyTransactions).length; + // 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 = storage.getItem(storageKeys.address); - const lastSharedIndex = storage.getItem(storageKeys.lastSharedIndex); - const lastGeneratedIndex = oldWalletUtil.getLastGeneratedIndex(); + const address = await wallet.storage.getCurrentAddress(); + const addressInfo = await wallet.storage.getAddressInfo(address); + const lastSharedIndex = addressInfo.bip32AddressIndex; store.dispatch(historyUpdate({ - historyTransactions, allTokens, lastSharedIndex, lastSharedAddress: address, - addressesFound: lastGeneratedIndex + 1, + addressesFound, transactionsFound, })); }, @@ -411,17 +411,20 @@ const wallet = { /** * Add passphrase to the wallet * + * @param {HathorWallet} wallet The wallet instance * @param {string} passphrase Passphrase to be added * @param {string} pin * @param {string} password * - * @return {string} words generated (null if words are not valid) + * @return {Promise} * @memberof Wallet * @inner */ - addPassphrase(passphrase, pin, password, routerHistory) { - const words = oldWalletUtil.getWalletWords(password); - this.cleanWallet() + async addPassphrase(wallet, passphrase, pin, password, routerHistory) { + const words = await wallet.storage.getWalletWords(password); + + // Clean wallet data, persisted data and redux + await this.cleanWallet(wallet); return this.generateWallet(words, passphrase, pin, password, routerHistory); }, @@ -430,11 +433,13 @@ const wallet = { * - Clean local storage * - Unsubscribe websocket connections * + * @param {HathorWallet} wallet The wallet instance * @memberof Wallet * @inner */ - cleanWallet() { - oldWalletUtil.cleanWallet(); + async cleanWallet(wallet) { + await wallet.storage.cleanStorage(true, true); + LOCAL_STORE.cleanWallet(); this.cleanWalletRedux(); }, @@ -451,38 +456,31 @@ const wallet = { /* * 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 */ - reloadData({endConnection = false} = {}) { + async reloadData(wallet, {endConnection = false} = {}) { store.dispatch(loadingAddresses(true)); - const dataToken = tokens.getTokens(); + 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: dataToken, + tokens, })); - // before cleaning data, check if we need to transfer xpubkey to wallet:accessData - const accessData = oldWalletUtil.getWalletAccessData(); - if (accessData.xpubkey === undefined) { - // XXX from v0.12.0 to v0.13.0, xpubkey changes from wallet:data to access:data. - // That's not a problem if wallet is being initialized. However, if it's already - // initialized, we need to set the xpubkey in the correct place. - const oldData = JSON.parse(localStorage.getItem(storageKeys.data)); - accessData.xpubkey = oldData.xpubkey; - oldWalletUtil.setWalletAccessData(accessData); - localStorage.removeItem(storageKeys.data); - } - // Load history from new server - const promise = oldWalletUtil.reloadData({endConnection}); - promise.then(() => { - this.afterLoadAddressHistory(); - }); - return promise; + await storageUtils.reloadStorage(wallet.storage, wallet.conn); + await this.afterLoadAddressHistory(wallet); }, /** @@ -504,7 +502,7 @@ const wallet = { * @inner */ allowSentry() { - storage.setItem(storageKeys.sentry, true); + LOCAL_STORE.setItem(storageKeys.sentry, true); this.updateSentryState(); }, @@ -515,7 +513,7 @@ const wallet = { * @inner */ disallowSentry() { - storage.setItem(storageKeys.sentry, false); + LOCAL_STORE.setItem(storageKeys.sentry, false); this.updateSentryState(); }, @@ -528,7 +526,7 @@ const wallet = { * @inner */ getSentryPermission() { - return storage.getItem(storageKeys.sentry); + return LOCAL_STORE.getItem(storageKeys.sentry); }, /** @@ -576,9 +574,7 @@ const wallet = { Object.entries(info).forEach( ([key, item]) => scope.setExtra(key, item) ); - DEBUG_LOCAL_DATA_KEYS.forEach( - (key) => scope.setExtra(key, storage.getItem(key)) - ) + // TODO: Add storage snapshot to sentry Sentry.captureException(error); }); }, @@ -608,7 +604,7 @@ const wallet = { * @inner */ isNotificationOn() { - return storage.getItem(storageKeys.notification) !== false; + return LOCAL_STORE.getItem(storageKeys.notification) !== false; }, /** @@ -618,7 +614,7 @@ const wallet = { * @inner */ turnNotificationOn() { - storage.setItem(storageKeys.notification, true); + LOCAL_STORE.setItem(storageKeys.notification, true); }, /** @@ -628,7 +624,7 @@ const wallet = { * @inner */ turnNotificationOff() { - storage.setItem(storageKeys.notification, false); + LOCAL_STORE.setItem(storageKeys.notification, false); }, /** @@ -640,7 +636,7 @@ const wallet = { * @inner */ areZeroBalanceTokensHidden() { - return storage.getItem(storageKeys.hideZeroBalanceTokens) === true; + return LOCAL_STORE.getItem(storageKeys.hideZeroBalanceTokens) === true; }, /** @@ -650,7 +646,7 @@ const wallet = { * @inner */ hideZeroBalanceTokens() { - storage.setItem(storageKeys.hideZeroBalanceTokens, true); + LOCAL_STORE.setItem(storageKeys.hideZeroBalanceTokens, true); // If the token selected has been hidden, then we must select HTR store.dispatch(resetSelectedTokenIfNeeded()); }, @@ -662,7 +658,7 @@ const wallet = { * @inner */ showZeroBalanceTokens() { - storage.setItem(storageKeys.hideZeroBalanceTokens, false); + LOCAL_STORE.setItem(storageKeys.hideZeroBalanceTokens, false); }, /** @@ -672,13 +668,13 @@ const wallet = { * @param {boolean} newValue If true, the token will always be shown */ setTokenAlwaysShow(tokenUid, newValue) { - const alwaysShowMap = storage.getItem(storageKeys.alwaysShowTokens) || {}; + const alwaysShowMap = LOCAL_STORE.getItem(storageKeys.alwaysShowTokens) || {}; if (!newValue) { delete alwaysShowMap[tokenUid]; } else { alwaysShowMap[tokenUid] = true; } - storage.setItem(storageKeys.alwaysShowTokens, alwaysShowMap); + LOCAL_STORE.setItem(storageKeys.alwaysShowTokens, alwaysShowMap); }, /** @@ -687,16 +683,20 @@ const wallet = { * @returns {boolean} */ isTokenAlwaysShow(tokenUid) { - const alwaysShowMap = storage.getItem(storageKeys.alwaysShowTokens) || {}; + const alwaysShowMap = LOCAL_STORE.getItem(storageKeys.alwaysShowTokens) || {}; return alwaysShowMap[tokenUid] || false; }, /** * Returns an array containing the uids of the tokens set to always be shown + * + * We use localStorage since the wallet storage is async and may require + * a refactor on how we load data in some components and screens. + * * @returns {string[]} */ listTokensAlwaysShow() { - const alwaysShowMap = storage.getItem(storageKeys.alwaysShowTokens) || {}; + const alwaysShowMap = LOCAL_STORE.getItem(storageKeys.alwaysShowTokens) || {};; return Object.keys(alwaysShowMap); }, @@ -723,7 +723,7 @@ const wallet = { * @returns {Record} */ getListenedProposals() { - const proposalMap = storage.getItem(storageKeys.atomicProposals); + const proposalMap = LOCAL_STORE.getItem(storageKeys.atomicProposals); return proposalMap || {}; }, @@ -732,7 +732,7 @@ const wallet = { * @param {Record} proposalList */ setListenedProposals(proposalList) { - storage.setItem(storageKeys.atomicProposals, proposalList); + LOCAL_STORE.setItem(storageKeys.atomicProposals, proposalList); }, } diff --git a/tests/env.js b/tests/env.js new file mode 100644 index 00000000..a03545bd --- /dev/null +++ b/tests/env.js @@ -0,0 +1,15 @@ +const Environment = require('jest-environment-jsdom'); + +/** + * A custom environment to set the TextEncoder that is required by TensorFlow.js. + */ +module.exports = class CustomTestEnvironment extends Environment { + async setup() { + await super.setup(); + if (typeof this.global.TextEncoder === 'undefined') { + const { TextEncoder, TextDecoder } = require('util'); + this.global.TextEncoder = TextEncoder; + this.global.TextDecoder = TextDecoder; + } + } +} From 5501807ce65e9a6b016001565d022d2143e96ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 17 Aug 2023 14:31:06 -0300 Subject: [PATCH 07/23] feat: register HTR on start saga (#407) * feat: register HTR on wallet start * feat: do not double handle tx events * feat: migrate localStorage from old versions * chore: bump in package * feat: enable change server while locked --- package-lock.json | 2 +- package.json | 2 +- public/electron.js | 2 +- src/App.js | 21 +++- src/actions/index.js | 26 ++++- src/components/ModalUnregisteredTokenInfo.js | 9 +- src/constants.js | 2 +- src/reducers/index.js | 48 ++++++++- src/sagas/helpers.js | 11 -- src/sagas/wallet.js | 105 +++++++++++++------ src/screens/LockedWallet.js | 3 + src/screens/Wallet.js | 9 +- src/utils/storage.js | 65 ++++++++++++ 13 files changed, 247 insertions(+), 58 deletions(-) create mode 100644 src/utils/storage.js diff --git a/package-lock.json b/package-lock.json index 9d625521..0dbbec28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.26.0", + "version": "0.27.0-rc1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 2b2bc4f8..84e463cb 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.26.0", + "version": "0.27.0-rc1", "private": true, "dependencies": { "@hathor/wallet-lib": "^1.0.0-rc8", diff --git a/public/electron.js b/public/electron.js index 8ee32b65..12802943 100644 --- a/public/electron.js +++ b/public/electron.js @@ -35,7 +35,7 @@ if (process.platform === 'darwin') { } const appName = 'Hathor Wallet'; -const walletVersion = '0.26.0'; +const walletVersion = '0.27.0-rc1'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/App.js b/src/App.js index 54c0a298..2320148a 100644 --- a/src/App.js +++ b/src/App.js @@ -34,6 +34,7 @@ import WalletVersionError from './screens/WalletVersionError'; import LoadWalletFailed from './screens/LoadWalletFailed'; import version from './utils/version'; import tokens from './utils/tokens'; +import storageUtils from './utils/storage'; import { connect } from 'react-redux'; import RequestErrorModal from './components/RequestError'; import store from './store/index'; @@ -159,13 +160,16 @@ class Root extends React.Component { * Validate if version is allowed for the loaded wallet */ const returnLoadedWalletComponent = (Component, props) => { + // For server screen we don't need to check version + // We also allow the server screen to be reached from the locked screen + // In the case of an unresponsive fullnode, which would block the wallet start + const isServerScreen = props.match.path === '/server'; + // If was closed and is loaded we need to redirect to locked screen - if (LOCAL_STORE.wasClosed() || LOCAL_STORE.isLocked()) { + if ((!isServerScreen) && (LOCAL_STORE.wasClosed() || LOCAL_STORE.isLocked())) { return ; } - // For server screen we don't need to check version - const isServerScreen = props.match.path === '/server'; const reduxState = store.getState(); // Check version @@ -216,6 +220,9 @@ const returnStartedRoute = (Component, props, rest) => { } } + // Detect a previous instalation and migrate before continuing + storageUtils.migratePreviousLocalStorage(); + // The wallet was not yet started, go to Welcome if (!LOCAL_STORE.wasStarted()) { return ; @@ -224,13 +231,17 @@ const returnStartedRoute = (Component, props, rest) => { // The wallet is already loaded const routeRequiresWalletToBeLoaded = rest.loaded; if (LOCAL_STORE.getWalletId()) { + // The server screen is a special case since we allow the user to change the + // connected server in case of unresponsiveness, this should be allowed from + // the locked screen since the wallet would not be able to be started otherwise + const isServerScreen = props.match.path === '/server'; // Wallet is locked, go to locked screen - if (LOCAL_STORE.isLocked()) { + if (LOCAL_STORE.isLocked() && !isServerScreen) { return ; } // Route requires the wallet to be loaded, render it - if (routeRequiresWalletToBeLoaded) { + if (routeRequiresWalletToBeLoaded || isServerScreen) { return returnLoadedWalletComponent(Component, props); } diff --git a/src/actions/index.js b/src/actions/index.js index 180181f1..4928b12f 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -42,6 +42,7 @@ export const types = { WALLET_STATE_READY: 'WALLET_STATE_READY', WALLET_STATE_ERROR: 'WALLET_STATE_ERROR', WALLET_RELOAD_DATA: 'WALLET_RELOAD_DATA', + WALLET_CHANGE_STATE: 'WALLET_CHANGE_STATE', WALLET_RESET: 'WALLET_RESET', WALLET_RESET_SUCCESS: 'WALLET_RESET_SUCCESS', WALLET_REFRESH_SHARED_ADDRESS: 'WALLET_REFRESH_SHARED_ADDRESS', @@ -51,6 +52,7 @@ export const types = { FEATURE_TOGGLE_INITIALIZED: 'FEATURE_TOGGLE_INITIALIZED', SET_FEATURE_TOGGLES: 'SET_FEATURE_TOGGLES', SET_UNLEASH_CLIENT: 'SET_UNLEASH_CLIENT', + UPDATE_TX_HISTORY: 'UPDATE_TX_HISTORY', }; /** @@ -154,7 +156,7 @@ export const resetWallet = () => ({ type: 'reset_wallet' }); * tokens {Array} array of token uids the the wallet has * currentAddress {Object} The current unused address */ -export const loadWalletSuccess = (tokens, currentAddress) => ({ type: 'load_wallet_success', payload: { tokens, currentAddress } }); +export const loadWalletSuccess = (tokens, registeredTokens, currentAddress) => ({ type: 'load_wallet_success', payload: { tokens, registeredTokens, currentAddress } }); /** * tx {Object} the new transaction @@ -482,3 +484,25 @@ export const walletResetSuccess = () => ({ export const walletReset = () => ({ type: types.WALLET_RESET, }); + +/** + * Action to update the token history with this tx. + * @param {Object} tx New transaction to update in the token history. + * @param {string} tokenId token to update the history. + * @param {number} balance transaction balance of the token on the wallet. + * @returns {Object} action to update the history of the token with the tx. + */ +export const updateTxHistory = (tx, tokenId, balance) => ({ + type: types.UPDATE_TX_HISTORY, + payload: { tx, tokenId, balance }, +}); + +/** + * Action to set the wallet state and allow the UI to react to a state change. + * @param {number} newState state from the enum included on HathorWallet + * @returns {Object} + */ +export const changeWalletState = (newState) => ({ + type: types.WALLET_CHANGE_STATE, + payload: newState, +}); diff --git a/src/components/ModalUnregisteredTokenInfo.js b/src/components/ModalUnregisteredTokenInfo.js index ad36e31b..3c1e836f 100644 --- a/src/components/ModalUnregisteredTokenInfo.js +++ b/src/components/ModalUnregisteredTokenInfo.js @@ -12,8 +12,13 @@ import tokens from '../utils/tokens'; import SpanFmt from './SpanFmt'; import TokenGeneralInfo from '../components/TokenGeneralInfo'; import hathorLib from '@hathor/wallet-lib'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +const mapStateToProps = (state) => { + return { storage: state.wallet.storage }; +}; + /** * Component that shows a modal with information about an unregistered token * @@ -62,7 +67,7 @@ class ModalUnregisteredTokenInfo extends React.Component { ); try { - const tokenData = await hathorLib.tokensUtils.validateTokenToAddByConfigurationString(configurationString, this.props.wallet.storage); + const tokenData = await hathorLib.tokensUtils.validateTokenToAddByConfigurationString(configurationString, this.props.storage); await tokens.addToken(tokenData.uid, tokenData.name, tokenData.symbol); $('#unregisteredTokenInfoModal').modal('hide'); this.props.tokenRegistered(this.props.token); @@ -153,4 +158,4 @@ ModalUnregisteredTokenInfo.propTypes = { totalSupply: PropTypes.number, }; -export default ModalUnregisteredTokenInfo; +export default connect(mapStateToProps)(ModalUnregisteredTokenInfo); diff --git a/src/constants.js b/src/constants.js index 6c3b29db..5dd0bf23 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,7 +20,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.26.0'; +export const VERSION = '0.27.0-rc1'; /** * Before this version the data in localStorage from the wallet is not compatible diff --git a/src/reducers/index.js b/src/reducers/index.js index 184651ea..c5ef2080 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -80,6 +80,7 @@ const initialState = { // Height of the best chain of the network arrived from ws data height: 0, wallet: null, + walletState: null, // Metadata of tokens tokenMetadata: {}, // Token list of uids that had errors when loading metadata @@ -266,6 +267,10 @@ const rootReducer = (state = initialState, action) => { return onFeatureToggleInitialized(state); case types.WALLET_RESET_SUCCESS: return onWalletResetSuccess(state); + case types.UPDATE_TX_HISTORY: + return onUpdateTxHistory(state, action); + case types.WALLET_CHANGE_STATE: + return onWalletStateChanged(state, action); default: return state; } @@ -303,7 +308,7 @@ const getTxHistoryFromWSTx = (tx, tokenUid, tokenTxBalance) => { balance: tokenTxBalance, is_voided: tx.is_voided, version: tx.version, - isAllAuthority: isAllAuthority(tx), + // isAllAuthority: isAllAuthority(tx), } }; @@ -336,7 +341,7 @@ const isAllAuthority = (tx) => { const onLoadWalletSuccess = (state, action) => { // Update the version of the wallet that the data was loaded LOCAL_STORE.setWalletVersion(VERSION); - const { tokens, currentAddress } = action.payload; + const { tokens, registeredTokens, currentAddress } = action.payload; const allTokens = new Set(tokens); return { @@ -345,6 +350,7 @@ const onLoadWalletSuccess = (state, action) => { lastSharedAddress: currentAddress.address, lastSharedIndex: currentAddress.index, allTokens, + tokens: registeredTokens, }; }; @@ -587,6 +593,7 @@ export const onTokenFetchBalanceRequested = (state, action) => { */ export const onTokenFetchBalanceSuccess = (state, action) => { const { tokenId, data } = action; + const oldState = get(state.tokensBalance, tokenId, {}); return { ...state, @@ -596,6 +603,7 @@ export const onTokenFetchBalanceSuccess = (state, action) => { status: TOKEN_DOWNLOAD_STATUS.READY, updatedAt: new Date().getTime(), data, + oldStatus: oldState.status, }, }, }; @@ -606,6 +614,7 @@ export const onTokenFetchBalanceSuccess = (state, action) => { */ export const onTokenFetchBalanceFailed = (state, action) => { const { tokenId } = action; + const oldState = get(state.tokensBalance, tokenId, {}); return { ...state, @@ -613,6 +622,7 @@ export const onTokenFetchBalanceFailed = (state, action) => { ...state.tokensBalance, [tokenId]: { status: TOKEN_DOWNLOAD_STATUS.FAILED, + oldStatus: oldState.status, }, }, }; @@ -624,6 +634,7 @@ export const onTokenFetchBalanceFailed = (state, action) => { */ export const onTokenFetchHistorySuccess = (state, action) => { const { tokenId, data } = action; + const oldState = get(state.tokensHistory, tokenId, {}); return { ...state, @@ -633,6 +644,7 @@ export const onTokenFetchHistorySuccess = (state, action) => { status: TOKEN_DOWNLOAD_STATUS.READY, updatedAt: new Date().getTime(), data, + oldStatus: oldState.status, }, }, }; @@ -643,6 +655,7 @@ export const onTokenFetchHistorySuccess = (state, action) => { */ export const onTokenFetchHistoryFailed = (state, action) => { const { tokenId } = action; + const oldState = get(state.tokensHistory, tokenId, {}); return { ...state, @@ -651,6 +664,7 @@ export const onTokenFetchHistoryFailed = (state, action) => { [tokenId]: { status: TOKEN_DOWNLOAD_STATUS.FAILED, data: [], + oldStatus: oldState.status, }, }, }; @@ -1014,5 +1028,35 @@ const onWalletResetSuccess = (state) => ({ unleashClient: state.unleashClient, }); +export const onUpdateTxHistory = (state, action) => { + const { tx, tokenId, balance } = action.payload; + const tokenHistory = state.tokensHistory[tokenId]; + + if (!tokenHistory) { + return state; + } + + for (const [index, histTx] of tokenHistory.data.entries()) { + if (histTx.tx_id === tx.tx_id) { + tokenHistory.data[index] = getTxHistoryFromWSTx(tx, tokenId, balance); + break; + } + } + + return { + ...state, + tokensHistory: { + ...state.tokensHistory, + [tokenId]: { + ...tokenHistory, + } + }, + }; +}; + +export const onWalletStateChanged = (state, { payload }) => ({ + ...state, + walletState: payload, +}); export default rootReducer; diff --git a/src/sagas/helpers.js b/src/sagas/helpers.js index 486935f9..a8044da5 100644 --- a/src/sagas/helpers.js +++ b/src/sagas/helpers.js @@ -107,17 +107,6 @@ export function errorHandler(saga, failureAction) { }; } -/** - * Get registered tokens from the wallet instance. - * @param {HathorWallet} wallet - * @param {boolean} excludeDefaultToken If we should exclude the default token. - * @returns {string[]} - */ -export async function getRegisteredTokensUids(wallet, excludeDefaultToken = false) { - const tokenConfigArr = await tokensUtils.getRegisteredTokens(wallet, excludeDefaultToken); - return tokenConfigArr.map(token => token.uid); -} - export function* dispatchLedgerTokenSignatureVerification(wallet) { const isHardware = yield wallet.storage.isHardwareWallet(); if (!isHardware) { diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index a40938cb..bdb446d8 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -58,22 +58,25 @@ import { proposalFetchRequested, walletResetSuccess, reloadWalletRequested, + changeWalletState, + updateTxHistory, } from '../actions'; import { specificTypeAndPayload, errorHandler, checkForFeatureFlag, - getRegisteredTokensUids, dispatchLedgerTokenSignatureVerification, } from './helpers'; import { fetchTokenData } from './tokens'; import walletUtils from '../utils/wallet'; +import tokensUtils from '../utils/tokens'; import { initializeSwapServiceBaseUrlForWallet } from "../utils/atomicSwap"; export const WALLET_STATUS = { READY: 'ready', FAILED: 'failed', LOADING: 'loading', + SYNCING: 'syncing', }; export function* isWalletServiceEnabled() { @@ -295,16 +298,18 @@ export function* startWallet(action) { } } + yield call([wallet.storage, wallet.storage.registerToken], hathorLibConstants.HATHOR_TOKEN_CONFIG); + if (hardware) { // This will verify all ledger trusted tokens to check their validity yield fork(dispatchLedgerTokenSignatureVerification, wallet); } try { - const { allTokens } = yield call(loadTokens); + const { allTokens, registeredTokens } = yield call(loadTokens); const currentAddress = yield call([wallet, wallet.getCurrentAddress]); // Store all tokens on redux - yield put(loadWalletSuccess(allTokens, currentAddress)); + yield put(loadWalletSuccess(allTokens, registeredTokens, currentAddress)); } catch(e) { yield put(startWalletFailed()); return; @@ -346,13 +351,13 @@ export function* loadTokens() { // Fetch all tokens, including the ones that are not registered yet const allTokens = yield call([wallet, wallet.getTokens]); - const registeredTokens = yield getRegisteredTokensUids(wallet, true); + const registeredTokens = yield call(tokensUtils.getRegisteredTokens, wallet); // We don't need to wait for the metadatas response, so we can just // spawn a new "thread" to handle it. // // `spawn` is similar to `fork`, but it creates a `detached` fork - yield spawn(fetchTokensMetadata, registeredTokens); + yield spawn(fetchTokensMetadata, registeredTokens.map(token => token.uid)); // Dispatch actions to asynchronously load the balances of each token the wallet has // ever interacted with. The `put` effect will just dispatch and continue, loading @@ -452,16 +457,7 @@ export function* listenForWalletReady(wallet) { } } -export function* handleTx(action) { - const tx = action.payload; - const wallet = yield select((state) => state.wallet); - const routerHistory = yield select((state) => state.routerHistory); - - if (!wallet.isReady()) { - return; - } - - // find tokens affected by the transaction +export function* handleTx(wallet, tx) { const affectedTokens = new Set(); for (const output of tx.outputs) { @@ -471,9 +467,48 @@ export function* handleTx(action) { for (const input of tx.inputs) { affectedTokens.add(input.token); } + + // We should refresh the available addresses. + // Since we have already received the transaction at this point, the wallet + // instance will already have updated its current address, we should just + // fetch it and update the redux-store + const newAddress = yield call([wallet, wallet.getCurrentAddress]); + + yield put(sharedAddressUpdate({ + lastSharedAddress: newAddress.address, + lastSharedIndex: newAddress.index, + })); + + return affectedTokens; +} + +export function* handleNewTx(action) { + const tx = action.payload; + const wallet = yield select((state) => state.wallet); + const routerHistory = yield select((state) => state.routerHistory); + + if (!wallet.isReady()) { + return; + } + + // reload token history of affected tokens + // We always reload the history and balance + const affectedTokens = yield call(handleTx, wallet, tx); const stateTokens = yield select((state) => state.tokens); const registeredTokens = stateTokens.map((token) => token.uid); + // We should download the **balance** and **history** for every token involved + // in the transaction + for (const tokenUid of affectedTokens) { + if (registeredTokens.indexOf(tokenUid) === -1) { + continue; + } + + yield put(tokenFetchBalanceRequested(tokenUid, true)); + yield put(tokenFetchHistoryRequested(tokenUid, true)); + } + + // We only show a notification on the first time we see a transaction let message = ''; if (transactionUtils.isBlock(tx)) { message = 'You\'ve found a new block! Click to open it.'; @@ -493,26 +528,31 @@ export function* handleTx(action) { routerHistory.push(`/transaction/${tx.tx_id}/`); } } +} - // We should refresh the available addresses. - // Since we have already received the transaction at this point, the wallet - // instance will already have updated its current address, we should just - // fetch it and update the redux-store - const newAddress = yield call([wallet, wallet.getCurrentAddress]); +export function* handleUpdateTx(action) { + const tx = action.payload; + const wallet = yield select((state) => state.wallet); + + if (!wallet.isReady()) { + return; + } + + // reload token history of affected tokens + // We always reload balance but only reload the tx being updated on history. + const affectedTokens = yield call(handleTx, wallet, tx); + const stateTokens = yield select((state) => state.tokens); + const registeredTokens = stateTokens.map((token) => token.uid); + const txbalance = yield call([wallet, wallet.getTxBalance], tx); - yield put(sharedAddressUpdate({ - lastSharedAddress: newAddress.address, - lastSharedIndex: newAddress.index, - })); - // We should download the **balance** and **history** for every token involved - // in the transaction for (const tokenUid of affectedTokens) { if (registeredTokens.indexOf(tokenUid) === -1) { continue; } + // Always reload balance yield put(tokenFetchBalanceRequested(tokenUid, true)); - yield put(tokenFetchHistoryRequested(tokenUid, true)); + yield put(updateTxHistory(tx, tokenUid, txbalance[tokenUid] || 0)); } } @@ -543,6 +583,8 @@ export function* setupWalletListeners(wallet) { data, })); + wallet.on('state', (data) => emitter(changeWalletState(data))); + return () => { wallet.conn.removeListener('best-block-update', listener); wallet.conn.removeListener('wallet-load-partial-update', listener); @@ -550,6 +592,7 @@ export function* setupWalletListeners(wallet) { wallet.removeListener('reload-data', listener); wallet.removeListener('update-tx', listener); wallet.removeListener('new-tx', listener); + wallet.removeListener('state', listener); }; }); @@ -624,7 +667,7 @@ export function* walletReloading() { try { // Store all tokens on redux as we might have lost tokens during the disconnected // period. - const { allTokens } = yield call(loadTokens); + const { allTokens, registeredTokens } = yield call(loadTokens); // We might have lost transactions during the reload, so we must invalidate the // token histories: @@ -649,7 +692,7 @@ export function* walletReloading() { const currentAddress = yield call([wallet, wallet.getCurrentAddress]); // Load success, we can send the user back to the wallet screen - yield put(loadWalletSuccess(allTokens, currentAddress)); + yield put(loadWalletSuccess(allTokens, registeredTokens, currentAddress)); routerHistory.replace('/wallet/'); yield put(loadingAddresses(false)); } catch (e) { @@ -715,8 +758,8 @@ export function* saga() { takeLatest('WALLET_CONN_STATE_UPDATE', onWalletConnStateUpdate), takeLatest('WALLET_RELOADING', walletReloading), takeLatest('WALLET_RESET', onWalletReset), - takeEvery('WALLET_NEW_TX', handleTx), - takeEvery('WALLET_UPDATE_TX', handleTx), + takeEvery('WALLET_NEW_TX', handleNewTx), + takeEvery('WALLET_UPDATE_TX', handleUpdateTx), takeEvery('WALLET_BEST_BLOCK_UPDATE', bestBlockUpdate), takeEvery('WALLET_PARTIAL_UPDATE', loadPartialUpdate), takeEvery('WALLET_RELOAD_DATA', walletReloading), diff --git a/src/screens/LockedWallet.js b/src/screens/LockedWallet.js index a5dc3676..e36554f1 100644 --- a/src/screens/LockedWallet.js +++ b/src/screens/LockedWallet.js @@ -80,6 +80,9 @@ class LockedWallet extends React.Component { } await LOCAL_STORE.handleDataMigration(pin); + // We block the wallet from being showed if it was locked or closed. + // So we need to mark it as opened for the UI to proceed. + LOCAL_STORE.open(); // LockedWallet screen was called for a result, so we should resolve the promise with the PIN after // it is validated. diff --git a/src/screens/Wallet.js b/src/screens/Wallet.js index dc62addd..26214738 100644 --- a/src/screens/Wallet.js +++ b/src/screens/Wallet.js @@ -50,6 +50,7 @@ const mapStateToProps = (state) => { tokenMetadata: state.tokenMetadata || {}, tokens: state.tokens, wallet: state.wallet, + walletState: state.walletState, useWalletService: state.useWalletService, }; }; @@ -459,8 +460,12 @@ class Wallet extends React.Component { const renderUnlockedWallet = () => { let template; - if (tokenHistory.status === TOKEN_DOWNLOAD_STATUS.LOADING - || tokenBalance.status === TOKEN_DOWNLOAD_STATUS.LOADING) { + /** + * We only show the loading message if we are syncing the entire history + * This will happen on the first history load and if we lose connection + * to the fullnode. + */ + if (this.props.walletState === hathorLib.HathorWallet.SYNCING) { template = (
Date: Wed, 23 Aug 2023 11:59:32 -0300 Subject: [PATCH 08/23] chore: bump wallet-lib v1.0.1 (#410) * chore: bump wallet-lib v1.0.1 * chore: update package-lock --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0dbbec28..2566bed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1600,9 +1600,9 @@ } }, "@hathor/wallet-lib": { - "version": "1.0.0-rc8", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.0-rc8.tgz", - "integrity": "sha512-Ti65+yYkSf5TMNHjS+iX4UQUfq2wPCGBufR5BRfq+PU7wAcxftEgSlZat/Qdcent9/fEE59V0bLg7uH1JeYvEQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.1.tgz", + "integrity": "sha512-Pfjq0oks3qxD2BJXNYv7X6jv0oIW0vdSBvpkLMVsKQ/VikXV2RHP+av2vQdnkC2lMdkvBeG/gtoREF21tQzing==", "requires": { "axios": "^0.21.4", "bitcore-lib": "^8.25.10", diff --git a/package.json b/package.json index 84e463cb..a8231998 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "version": "0.27.0-rc1", "private": true, "dependencies": { - "@hathor/wallet-lib": "^1.0.0-rc8", + "@hathor/wallet-lib": "^1.0.1", "@ledgerhq/hw-transport-node-hid": "^6.27.1", "@sentry/electron": "^3.0.7", "babel-polyfill": "^6.26.0", From 9a477a368bc10415325b29d2e52dc9ed566731fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 23 Aug 2023 14:50:58 -0300 Subject: [PATCH 09/23] feat: add isAllAuthority on all redux transactions (#409) --- src/reducers/index.js | 25 ++------------- src/sagas/tokens.js | 4 +-- src/utils/helpers.js | 75 ++++++++++++++++++++++++++++++++++++++++--- src/utils/wallet.js | 2 +- 4 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/reducers/index.js b/src/reducers/index.js index c5ef2080..cc7b69a2 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -13,6 +13,7 @@ import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; import { WALLET_STATUS } from '../sagas/wallet'; import { PROPOSAL_DOWNLOAD_STATUS } from '../utils/atomicSwap'; import { HATHOR_TOKEN_CONFIG } from "@hathor/wallet-lib/lib/constants"; +import helpersUtils from '../utils/helpers'; import LOCAL_STORE from '../storage'; /** @@ -308,32 +309,10 @@ const getTxHistoryFromWSTx = (tx, tokenUid, tokenTxBalance) => { balance: tokenTxBalance, is_voided: tx.is_voided, version: tx.version, - // isAllAuthority: isAllAuthority(tx), + isAllAuthority: helpersUtils.isAllAuthority(tx), } }; -/** - * Check if the tx has only inputs and outputs that are authorities - * - * @param {Object} tx Transaction data - * - * @return {boolean} If the tx has only authority - */ -const isAllAuthority = (tx) => { - for (let txin of tx.inputs) { - if (!hathorLib.transactionUtils.isAuthorityOutput(txin)) { - return false; - } - } - - for (let txout of tx.outputs) { - if (!hathorLib.transactionUtils.isAuthorityOutput(txout)) { - return false; - } - } - - return true; -} /** * Got wallet history. Update wallet data on redux diff --git a/src/sagas/tokens.js b/src/sagas/tokens.js index 46335800..1c148e1e 100644 --- a/src/sagas/tokens.js +++ b/src/sagas/tokens.js @@ -201,8 +201,8 @@ function* fetchTokenHistory(action) { return; } - const response = yield call(wallet.getTxHistory.bind(wallet), { token_id: tokenId }); - const data = response.map((txHistory) => helpers.mapTokenHistory(txHistory, tokenId)); + const response = yield call([wallet, wallet.getTxHistory], { token_id: tokenId }); + const data = yield call([helpers, helpers.mapTokenHistory], wallet, response, tokenId); yield put(tokenFetchHistorySuccess(tokenId, data)); } catch (e) { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 93e2a65a..9424c336 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -187,12 +187,62 @@ const helpers = { }, /** - * Map token history object to the expected object in the wallet redux data + * @typedef {Object} ReduxTxHistory + * @property {string} tx_id + * @property {number} timestamp + * @property {string} tokenUid + * @property {number} balance + * @property {boolean} is_voided + * @property {number} version + * @property {boolean} isAllAuthority + */ + + /** + * @typedef {Object} LibTxHistory + * @property {string} txId + * @property {number} balance + * @property {number} timestamp + * @property {boolean} voided + * @property {number} version + */ + + /** + * Check if the tx has only inputs and outputs that are authorities + * + * @param {Object} tx Transaction data * - * tx {Object} history data element - * tokenUid {String} token uid + * @return {boolean} If the tx has only authority */ - mapTokenHistory(tx, tokenUid) { + isAllAuthority(tx) { + for (let txin of tx.inputs) { + if (!hathorLib.transactionUtils.isAuthorityOutput(txin)) { + return false; + } + } + + for (let txout of tx.outputs) { + if (!hathorLib.transactionUtils.isAuthorityOutput(txout)) { + return false; + } + } + + return true; + }, + + /** + * Map tx history object to the expected object in the wallet redux data + * + * @param {HathorWallet} wallet - Wallet instance + * @param {LibTxHistory} tx - tx received via getTxHistory + * @param {string} tokenUid - token uid + * @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); + return { tx_id: tx.txId, timestamp: tx.timestamp, @@ -201,9 +251,26 @@ const helpers = { // in wallet service this comes as 0/1 and in the full node comes with true/false is_voided: Boolean(tx.voided), version: tx.version, + isAllAuthority, }; }, + /** + * Map token history to a list of the expected format in the wallet redux + * + * @param {HathorWallet} wallet - Wallet instance + * @param {LibTxHistory[]} history - history of txs received via getTxHistory + * @param {string} tokenUid - token uid + * @returns {Promise} + */ + async mapTokenHistory(wallet, history, tokenUid) { + const mappedHistory = []; + for (const tx of history) { + mappedHistory.push(await this.mapTxHistoryToRedux(wallet, tx, tokenUid)); + } + return mappedHistory; + }, + /** * Returns the current OS * diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 331d040e..c6fc6f6a 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -157,7 +157,7 @@ const wallet = { */ async fetchMoreHistory(wallet, token, history) { const newHistory = await wallet.getTxHistory({ token_id: token, skip: history.length, count: WALLET_HISTORY_COUNT }); - const newHistoryObjects = newHistory.map((element) => helpers.mapTokenHistory(element, token)); + const newHistoryObjects = await helpers.mapTokenHistory(wallet, newHistory, token); if (newHistoryObjects.length) { store.dispatch(updateTokenHistory(token, newHistoryObjects)); From cef3df71c664c818956fdc2a47668511d040138e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 14 Sep 2023 14:31:11 -0300 Subject: [PATCH 10/23] fix: change server (#411) * fix: remove registered tokens when changing networks * chore: wallet-lib bump * fix: remove testnet suffix when configuring the network * fix: tx balance not being calculated correctly * fix: unexpected state on error --- package-lock.json | 6 +++--- package.json | 2 +- src/reducers/index.js | 16 +++++++++------- src/sagas/wallet.js | 10 +++++++++- src/screens/LoadWallet.js | 1 - src/screens/Server.js | 24 +++++++++++++++--------- src/screens/TransactionDetail.js | 19 +++++++++++++++++++ src/storage.js | 10 ++++++++++ src/utils/wallet.js | 12 +++++++++--- 9 files changed, 75 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2566bed3..35f7b5c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1600,9 +1600,9 @@ } }, "@hathor/wallet-lib": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.1.tgz", - "integrity": "sha512-Pfjq0oks3qxD2BJXNYv7X6jv0oIW0vdSBvpkLMVsKQ/VikXV2RHP+av2vQdnkC2lMdkvBeG/gtoREF21tQzing==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.2.tgz", + "integrity": "sha512-jjfP9rseY0fGyLa94BxD8oV0xHVv9SymrqlE31le9DWxunaf3JhC/3Bk4K8NkJb2SBhrGVhtb/X92J97jXdmhQ==", "requires": { "axios": "^0.21.4", "bitcore-lib": "^8.25.10", diff --git a/package.json b/package.json index a8231998..1e626621 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "version": "0.27.0-rc1", "private": true, "dependencies": { - "@hathor/wallet-lib": "^1.0.1", + "@hathor/wallet-lib": "^1.0.2", "@ledgerhq/hw-transport-node-hid": "^6.27.1", "@sentry/electron": "^3.0.7", "babel-polyfill": "^6.26.0", diff --git a/src/reducers/index.js b/src/reducers/index.js index cc7b69a2..0ab02650 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -972,13 +972,15 @@ export const onStoreRouterHistory = (state, action) => { }; }; -const onSetServerInfo = (state, action) => ({ - ...state, - serverInfo: { - network: action.payload.network, - version: action.payload.version, - }, -}); +const onSetServerInfo = (state, action) => { + return { + ...state, + serverInfo: { + network: action.payload.network, + version: action.payload.version, + }, + } +}; const onFeatureToggleInitialized = (state) => ({ ...state, diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index bdb446d8..9dcc6b3e 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -151,6 +151,8 @@ export function* startWallet(action) { if (useWalletService) { // 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 + config.setWalletServiceBaseUrl(LOCAL_STORE.getWsServer()); config.getWalletServiceBaseUrl(); } catch(err) { if (err instanceof hathorErrors.GetWalletServiceUrlError) { @@ -246,7 +248,7 @@ export function* startWallet(action) { if (serverInfo) { version = serverInfo.version; - networkName = serverInfo.network; + networkName = serverInfo.network && serverInfo.network.split('-')[0]; } yield put(setServerInfo({ @@ -266,6 +268,12 @@ export function* startWallet(action) { // takeLatest will stop running the generator if a new START_WALLET_REQUESTED // action is dispatched, but returning so the code is clearer return; + } else { + console.error(e); + // Return to locked screen when the wallet fails to start + LOCAL_STORE.lock(); + routerHistory.push('/'); + return } } diff --git a/src/screens/LoadWallet.js b/src/screens/LoadWallet.js index 9c8cea4a..3025e309 100644 --- a/src/screens/LoadWallet.js +++ b/src/screens/LoadWallet.js @@ -97,7 +97,6 @@ class LoadWallet extends React.Component { // Clean pin and password from redux this.props.updatePassword(null); this.props.updatePin(null); - this.props.history.push('/wallet/'); } /** diff --git a/src/screens/Server.js b/src/screens/Server.js index e19b5194..21a3aa4d 100644 --- a/src/screens/Server.js +++ b/src/screens/Server.js @@ -147,8 +147,10 @@ class Server extends React.Component { hathorLib.config.getWalletServiceBaseWsUrl() : ''; + const currentNetwork = this.props.wallet.getNetwork(); + // Update new server in storage and in the config singleton - await this.props.wallet.changeServer(newBaseServer); + this.props.wallet.changeServer(newBaseServer); // We only have a different websocket server on the wallet-service facade, so update the config singleton if (this.props.useWalletService) { @@ -178,9 +180,9 @@ class Server extends React.Component { // Go back to the previous server // If the user decides to continue with this change, we will update again - await this.props.wallet.changeServer(currentServer); + this.props.wallet.changeServer(currentServer); if (this.props.useWalletService) { - await this.props.wallet.changeWsServer(currentWsServer); + await this.props.wallet.changeWsServer(currentWsServer); } LOCAL_STORE.setServers( currentServer, @@ -192,13 +194,14 @@ class Server extends React.Component { this.setState({ loading: false }); } else { // We are on mainnet, so set the network on the singleton and storage + const networkChanged = LOCAL_STORE.getNetwork() !== 'mainnet'; hathorLib.config.setNetwork('mainnet'); helpers.updateNetwork('mainnet'); - this.executeServerChange(); + this.executeServerChange(networkChanged); } } catch (e) { // Go back to the previous server - await this.props.wallet.changeServer(currentServer); + this.props.wallet.changeServer(currentServer); if (this.props.useWalletService) { await this.props.wallet.changeWsServer(currentWsServer); } @@ -206,6 +209,7 @@ class Server extends React.Component { currentServer, this.props.useWalletService ? currentWsServer : null, ); + helpers.updateNetwork(currentNetwork); this.setState({ loading: false, errorMessage: e.message, @@ -219,7 +223,7 @@ class Server extends React.Component { * so we can execute the change */ confirmTestnetServer = async () => { - await this.props.wallet.changeServer(this.state.selectedServer); + this.props.wallet.changeServer(this.state.selectedServer); if (this.props.useWalletService) { await this.props.wallet.changeWsServer(this.state.selectedWsServer); } @@ -228,6 +232,8 @@ class Server extends React.Component { this.props.useWalletService ? this.state.selectedWsServer : null, ); + const networkChanged = !LOCAL_STORE.getNetwork().startsWith('testnet'); + // Set network on config singleton so the load wallet will get it properly hathorLib.config.setNetwork(this.state.selectedNetwork); // Store on localStorage @@ -236,18 +242,18 @@ class Server extends React.Component { this.setState({ loading: true, }); - this.executeServerChange(); + this.executeServerChange(networkChanged); } /** * Execute server change checking server API and, in case of success * reloads data and redirects to wallet screen */ - executeServerChange = async () => { + executeServerChange = async (networkChanged) => { // We don't have PIN on hardware wallet const pin = LOCAL_STORE.isHardwareWallet() ? null : this.refs.pin.value; try { - await wallet.changeServer(this.props.wallet, pin, this.props.history); + await wallet.changeServer(this.props.wallet, pin, this.props.history, networkChanged); this.props.history.push('/wallet/'); } catch (err) { this.setState({ diff --git a/src/screens/TransactionDetail.js b/src/screens/TransactionDetail.js index 8143e836..2af1d117 100644 --- a/src/screens/TransactionDetail.js +++ b/src/screens/TransactionDetail.js @@ -94,6 +94,25 @@ class TransactionDetail extends React.Component { try { const data = await this.props.wallet.getFullTxById(this.props.match.params.id); + for (const output of data.tx.outputs) { + if (!output.token) { + if (output.token_data === 0) { + output.token = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; + } else { + output.token = data.tx.tokens[(output.token_data & hathorLib.constants.TOKEN_INDEX_MASK) -1].uid; + } + } + } + + for (const input of data.tx.inputs) { + if (!input.token) { + if (input.token_data === 0) { + input.token = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; + } else { + input.token = data.tx.tokens[(input.token_data & hathorLib.constants.TOKEN_INDEX_MASK) -1].uid; + } + } + } if (!hathorLib.transactionUtils.isBlock(data.tx)) { this.getConfirmationData(); diff --git a/src/storage.js b/src/storage.js index 5bff56f0..2d93440f 100644 --- a/src/storage.js +++ b/src/storage.js @@ -226,6 +226,16 @@ export class LocalStorageStore { return decryptedWords.toString(CryptoJS.enc.Utf8); } + async getWalletWords(password) { + const storage = this.getStorage(); + if (!storage) { + throw new Error('Cannot get words from uninitialized wallet'); + } + + const data = await storage.getAccessData(); + return cryptoUtils.decryptData(data.words, password); + } + /** * Migrate registered tokens from the old storage into the new storage * The old storage holds an array of token data and the new storage expects diff --git a/src/utils/wallet.js b/src/utils/wallet.js index c6fc6f6a..0309293b 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -353,8 +353,14 @@ const wallet = { * @param {string} pin The pin entered by the user * @param {any} routerHistory */ - async changeServer(wallet, pin, routerHistory) { - await wallet.stop({ cleanStorage: false }); + async changeServer(wallet, pin, routerHistory, networkChanged) { + // We only clean the storage if the network has changed + await wallet.stop({ cleanStorage: true, cleanAddresses: true }); + + if (networkChanged) { + // need to clean the storage, including registered tokens. + await wallet.storage.cleanStorage(true, true, true); + } const isHardwareWallet = await wallet.isHardwareWallet(); @@ -421,7 +427,7 @@ const wallet = { * @inner */ async addPassphrase(wallet, passphrase, pin, password, routerHistory) { - const words = await wallet.storage.getWalletWords(password); + const words = await LOCAL_STORE.getWalletWords(password); // Clean wallet data, persisted data and redux await this.cleanWallet(wallet); From d512857237c4096e7f48cbcc1f4da53a5087d5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 14 Sep 2023 18:09:06 -0300 Subject: [PATCH 11/23] chore: bump version v0.27.0-rc2 (#412) --- 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 35f7b5c8..f6f95a27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.27.0-rc1", + "version": "0.27.0-rc2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1e626621..a9ea643c 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.0-rc1", + "version": "0.27.0-rc2", "private": true, "dependencies": { "@hathor/wallet-lib": "^1.0.2", diff --git a/public/electron.js b/public/electron.js index 12802943..a3586963 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.0-rc1'; +const walletVersion = '0.27.0-rc2'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/constants.js b/src/constants.js index 5dd0bf23..59a506ac 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,7 +20,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.27.0-rc1'; +export const VERSION = '0.27.0-rc2'; /** * Before this version the data in localStorage from the wallet is not compatible From 24061091decf61c0e93ca56bfd3675c1ccf9a80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 25 Sep 2023 15:17:00 -0300 Subject: [PATCH 12/23] fix: issues found during the v0.27.0-rc2 qa (#413) * fix: wait for new address * fix: load network from storage into lib * fix: changing passphrase should automatically start the wallet * chore: remove zero balance when unregistering token (#414) --- src/App.js | 35 ++++++++++++++++++++++----------- src/components/WalletAddress.js | 4 ++-- src/reducers/index.js | 16 +++++++++++---- src/sagas/helpers.js | 1 + src/sagas/wallet.js | 2 +- src/screens/LoadWallet.js | 1 + src/storage.js | 5 ++--- src/utils/helpers.js | 31 +++++++++++++++++++++++++++++ src/utils/version.js | 2 +- src/utils/wallet.js | 14 +++++++++---- 10 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/App.js b/src/App.js index 2320148a..3fef756a 100644 --- a/src/App.js +++ b/src/App.js @@ -33,6 +33,7 @@ import VersionError from './screens/VersionError'; import WalletVersionError from './screens/WalletVersionError'; import LoadWalletFailed from './screens/LoadWalletFailed'; import version from './utils/version'; +import helpers from './utils/helpers'; import tokens from './utils/tokens'; import storageUtils from './utils/storage'; import { connect } from 'react-redux'; @@ -170,10 +171,16 @@ const returnLoadedWalletComponent = (Component, props) => { return ; } + if (isServerScreen) { + // We allow server screen to be shown from locked screen to allow the user to + // change the server before from a locked wallet. + return returnDefaultComponent(Component, props); + } + const reduxState = store.getState(); // Check version - if (reduxState.isVersionAllowed === undefined && !isServerScreen) { + if (reduxState.isVersionAllowed === undefined) { // We already handle all js errors in general and open an error modal to the user // so there is no need to catch the promise error below version.checkApiVersion(reduxState.wallet); @@ -182,19 +189,20 @@ const returnLoadedWalletComponent = (Component, props) => { state: {path: props.match.url}, waitVersionCheck: true }} />; - } else if (reduxState.isVersionAllowed === false && !isServerScreen) { + } + if (reduxState.isVersionAllowed === false) { return ; - } else { - if (reduxState.loadingAddresses && !isServerScreen) { - // If wallet is still loading addresses we redirect to the loading screen - return ; - } else { - return returnDefaultComponent(Component, props); - } } + + // The version has been checked and allowed + if (reduxState.loadingAddresses) { + // If wallet is still loading addresses we redirect to the loading screen + return ; + } + return returnDefaultComponent(Component, props); } /** @@ -286,6 +294,9 @@ const returnDefaultComponent = (Component, props) => { const reduxState = store.getState(); if (reduxState.isVersionAllowed === undefined) { + + helpers.loadStorageState(); + // We already handle all js errors in general and open an error modal to the user // so there is no need to catch the promise error below version.checkApiVersion(reduxState.wallet); diff --git a/src/components/WalletAddress.js b/src/components/WalletAddress.js index 43929c64..c269cb96 100644 --- a/src/components/WalletAddress.js +++ b/src/components/WalletAddress.js @@ -69,9 +69,9 @@ export class WalletAddress extends React.Component { * * @param {Object} e Event emitted by the link clicked */ - generateNewAddress = (e) => { + generateNewAddress = async (e) => { e.preventDefault(); - const address = this.props.wallet.getNextAddress(); + const address = await this.props.wallet.getNextAddress(); if (address.address === this.props.lastSharedAddress) { this.alertErrorRef.current.show(3000); diff --git a/src/reducers/index.js b/src/reducers/index.js index 0ab02650..86ad2fd3 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -372,14 +372,13 @@ const onUpdateLoadedData = (state, action) => ({ }); const onCleanData = (state) => { - if (state.wallet) { - state.wallet.stop(); - } - return Object.assign({}, initialState, { isVersionAllowed: state.isVersionAllowed, loadingAddresses: state.loadingAddresses, ledgerWasClosed: state.ledgerWasClosed, + // Keep the unleashClient as it should continue running + unleashClient: state.unleashClient, + featureTogglesInitialized: state.featureTogglesInitialized, }); }; @@ -451,6 +450,15 @@ const removeTokenMetadata = (state, action) => { delete newMeta[uid]; } + // If the token has zero balance we should remove the balance data + const newBalance = Object.assign({}, state.tokensBalance); + if (uid in newBalance && (!!newBalance[uid].data)) { + const balance = newBalance[uid].data; + if ((balance.unlocked + balance.locked) === 0) { + delete newBalance[uid]; + } + } + return { ...state, tokenMetadata: newMeta, diff --git a/src/sagas/helpers.js b/src/sagas/helpers.js index a8044da5..cf4f95b1 100644 --- a/src/sagas/helpers.js +++ b/src/sagas/helpers.js @@ -20,6 +20,7 @@ export function* waitForFeatureToggleInitialization() { const featureTogglesInitialized = yield select((state) => state.featureTogglesInitialized); if (!featureTogglesInitialized) { + console.log('Feature toggle is not initialized, will wait indefinetely until it is.'); // Wait until featureToggle saga completed initialization, which includes // downloading the current toggle status for this client. yield take(types.FEATURE_TOGGLE_INITIALIZED); diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index 9dcc6b3e..a0a04e0a 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -120,7 +120,7 @@ export function* startWallet(action) { if (hardware) { yield LOCAL_STORE.initHWStorage(xpub); } else { - yield LOCAL_STORE.initStorage(words, password, pin); + yield LOCAL_STORE.initStorage(words, password, pin, passphrase); } } diff --git a/src/screens/LoadWallet.js b/src/screens/LoadWallet.js index 3025e309..0d450b99 100644 --- a/src/screens/LoadWallet.js +++ b/src/screens/LoadWallet.js @@ -91,6 +91,7 @@ class LoadWallet extends React.Component { pinSuccess = () => { // Getting redux variables before cleaning all data const { pin, password } = this.props; + LOCAL_STORE.unlock(); // First we clean what can still be there of a last wallet wallet.generateWallet(this.state.words, '', pin, password, this.props.history); LOCAL_STORE.markBackupDone(); diff --git a/src/storage.js b/src/storage.js index 2d93440f..1d116cdc 100644 --- a/src/storage.js +++ b/src/storage.js @@ -89,8 +89,6 @@ export class LocalStorageStore { cleanWallet() { this.removeItem('wallet:id'); this.removeItem(IS_HARDWARE_KEY); - this.removeItem(STARTED_KEY); - this.removeItem(LOCKED_KEY); this.removeItem(CLOSED_KEY); delete this._storage; this._storage = null; @@ -104,13 +102,14 @@ export class LocalStorageStore { } } - async initStorage(seed, password, pin) { + async initStorage(seed, password, pin, passphrase='') { this._storage = null; this.setHardwareWallet(false); const accessData = walletUtils.generateAccessDataFromSeed( seed, { pin, + passphrase, password, networkName: config.getNetwork().name, } diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 9424c336..16f0660c 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -40,6 +40,37 @@ const helpers = { } }, + /** + * Load the network and server url from localstorage into hathorlib and redux. + * If not configured, get the default from hathorlib. + */ + loadStorageState() { + let network = LOCAL_STORE.getNetwork(); + if (!network) { + network = hathorLib.config.getNetwork().name; + } + + // Update the network in redux and lib + this.updateNetwork(network); + + let server = LOCAL_STORE.getServer(); + if (!server) { + server = hathorLib.config.getServerUrl(); + } + hathorLib.config.setServerUrl(server); + + let wsServer = LOCAL_STORE.getWsServer(); + if (wsServer) { + hathorLib.config.setWalletServiceBaseWsUrl(wsServer); + const storage = LOCAL_STORE.getStorage(); + if (storage) { + // This is a promise but we should not await it since this method has to be sync + // There is no issue not awaiting this since we already have this configured on the config + storage.store.setItem('wallet:wallet_service:ws_server', wsServer); + } + } + }, + /** * Update network variables in redux, storage and lib * diff --git a/src/utils/version.js b/src/utils/version.js index 9d70f2fc..f2860285 100644 --- a/src/utils/version.js +++ b/src/utils/version.js @@ -22,7 +22,7 @@ const version = { /** * Checks if the API version of the server the wallet is connected is valid for this wallet version * - * @param {HathorWallet | HathorWalletServiceWallet} wallet - Current wallet instance + * @param {HathorWallet | HathorWalletServiceWallet | undefined} wallet - Current wallet instance * @return {Promise} Promise that resolves after getting the version and updating Redux * * @memberof Version diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 0309293b..9adf43ea 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -131,6 +131,7 @@ const wallet = { try { walletUtils.wordsValid(words); } catch(e) { + console.error(e); if (e instanceof hathorErrors.InvalidWords) { return null; } else { @@ -431,7 +432,8 @@ const wallet = { // Clean wallet data, persisted data and redux await this.cleanWallet(wallet); - return this.generateWallet(words, passphrase, pin, password, routerHistory); + helpers.loadStorageState(); + this.generateWallet(words, passphrase, pin, password, routerHistory); }, /* @@ -446,16 +448,20 @@ const wallet = { async cleanWallet(wallet) { await wallet.storage.cleanStorage(true, true); LOCAL_STORE.cleanWallet(); - this.cleanWalletRedux(); + this.cleanWalletRedux(wallet); }, - /* + /** * Clean data from redux * + * @param {HathorWallet|undefined} wallet The wallet instance * @memberof Wallet * @inner */ - cleanWalletRedux() { + cleanWalletRedux(wallet) { + if (wallet) { + wallet.stop(); + } store.dispatch(cleanData()); }, From 503d91c701e1a98a358c7a11a426f16cd2a5d0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 25 Sep 2023 23:19:07 -0300 Subject: [PATCH 13/23] chore: stop e2e tests (#416) * chore: update cypress actions to newer versions * chore: remove triggers for e2e tests --- .github/workflows/e2e.yml | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f60e75cc..8a775e61 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,15 +1,15 @@ name: End-to-end tests -on: - push: - branches: - - master - - dev - tags: - - v* - pull_request: - branches: - - dev - - master +# on: +# push: +# branches: +# - master +# - dev +# tags: +# - v* +# pull_request: +# branches: +# - dev +# - master jobs: cypress-run: runs-on: ubuntu-20.04 @@ -18,15 +18,18 @@ jobs: node-version: [14.x] steps: - name: Checkout - uses: actions/checkout@v2 + # https://github.com/actions/checkout/releases/tag/v4.0.0 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - name: Setup nodejs - uses: actions/setup-node@v2 + # https://github.com/actions/setup-node/releases/tag/v3.8.1 + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d with: node-version: ${{ matrix.node-version }} # Install NPM dependencies, cache them correctly # and run all Cypress tests - name: Cypress run - uses: cypress-io/github-action@v5 + # https://github.com/cypress-io/github-action/releases/tag/v6.5.0 + uses: cypress-io/github-action@59810ebfa5a5ac6fcfdcfdf036d1cd4d083a88f2 with: start: npm start wait-on: 'http://localhost:3000' From cd2888cad0b72a23a3905c7a54964fda4491773c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 26 Sep 2023 09:02:32 -0300 Subject: [PATCH 14/23] chore: bump version v0.27.0-rc3 (#417) --- 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 f6f95a27..44749b9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.27.0-rc2", + "version": "0.27.0-rc3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a9ea643c..fabe1c0f 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.0-rc2", + "version": "0.27.0-rc3", "private": true, "dependencies": { "@hathor/wallet-lib": "^1.0.2", diff --git a/public/electron.js b/public/electron.js index a3586963..89bd8165 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.0-rc2'; +const walletVersion = '0.27.0-rc3'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/constants.js b/src/constants.js index 59a506ac..e2585451 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,7 +20,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.27.0-rc2'; +export const VERSION = '0.27.0-rc3'; /** * Before this version the data in localStorage from the wallet is not compatible From a36a7fe5404a072bb21e584fb6db732dd49577f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 16 Oct 2023 13:06:30 -0300 Subject: [PATCH 15/23] feat: changes from v0.27.0-rc3 QA (#418) * feat: fix key migration from previous version * chore: use correct ledger version when testing if we should allow custom tokens * chore: add docstrings --- src/App.js | 2 +- src/sagas/helpers.js | 2 +- src/storage.js | 26 +++++++++++++++++++++++--- src/utils/storage.js | 19 ++++++++++++++++++- src/utils/version.js | 2 +- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/App.js b/src/App.js index 3fef756a..74d66a30 100644 --- a/src/App.js +++ b/src/App.js @@ -238,7 +238,7 @@ const returnStartedRoute = (Component, props, rest) => { // The wallet is already loaded const routeRequiresWalletToBeLoaded = rest.loaded; - if (LOCAL_STORE.getWalletId()) { + if (LOCAL_STORE.isLoadedSync()) { // The server screen is a special case since we allow the user to change the // connected server in case of unresponsiveness, this should be allowed from // the locked screen since the wallet would not be able to be started otherwise diff --git a/src/sagas/helpers.js b/src/sagas/helpers.js index cf4f95b1..4f58d3b1 100644 --- a/src/sagas/helpers.js +++ b/src/sagas/helpers.js @@ -9,7 +9,7 @@ import { import { types } from '../actions'; import { FEATURE_TOGGLE_DEFAULTS } from '../constants'; import tokensUtils from '../utils/tokens'; -import version from '../utils/tokens'; +import version from '../utils/version'; import ledger from '../utils/ledger'; import LOCAL_STORE from '../storage'; diff --git a/src/storage.js b/src/storage.js index 1d116cdc..3ab26c8a 100644 --- a/src/storage.js +++ b/src/storage.js @@ -82,6 +82,19 @@ export class LocalStorageStore { return this.getItem('wallet:id'); } + /** + * Check if the wallet is loaded, it does not check the access data in storage since it requires + * an async call, so we only check the wallet id. + * We also check the 'wallet:accessData' key used on old versions of the lib so we can start the + * wallet and finish the migration process, this key is deleted during the migration process so + * this check will not be needed after all wallets have migrated. + * + * @return {boolean} Whether the wallet is loaded + */ + isLoadedSync() { + return (!!this.getWalletId()) || (!!this.getItem('wallet:accessData')) + } + setWalletId(walletId) { this.setItem('wallet:id', walletId); } @@ -244,6 +257,9 @@ export class LocalStorageStore { */ async handleMigrationOldRegisteredTokens(storage) { const oldTokens = this.getItem('wallet:tokens'); + if (!oldTokens) { + return; + } for (const token of oldTokens) { await storage.registerToken(token); } @@ -277,9 +293,13 @@ export class LocalStorageStore { } // The access data is saved on the new storage, we can delete the old data. - // This will only delete keys with the wallet prefix, so we don't delete - // the biometry keys and new data. - await this.clearItems(true); + // This will only delete keys with the wallet prefix + for (const key of Object.keys(localStorage)) { + if (key === 'wallet:id') continue; + if (key.startsWith('wallet:')) { + localStorage.removeItem(key); + } + } } // We have finished the migration so we can set the storage version to the most recent one. this.updateStorageVersion(); diff --git a/src/utils/storage.js b/src/utils/storage.js index cd6265aa..f11a1b5c 100644 --- a/src/utils/storage.js +++ b/src/utils/storage.js @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import LOCAL_STORE, { STARTED_KEY, LOCKED_KEY, CLOSED_KEY, IS_BACKUP_DONE_KEY, IS_HARDWARE_KEY } from '../storage'; +import LOCAL_STORE, { STARTED_KEY, LOCKED_KEY, CLOSED_KEY, IS_BACKUP_DONE_KEY, IS_HARDWARE_KEY, SERVER_KEY } from '../storage'; + +import helpers from './helpers'; const storageUtils = { /** @@ -22,6 +24,21 @@ const storageUtils = { OLD_CLOSED_KEY: 'wallet:closed', OLD_BACKUP_KEY: 'wallet:backup', OLD_TYPE_KEY: 'wallet:type', + OLD_NETWORK_KEY: 'wallet:network', + OLD_SERVER_KEY: 'wallet:server', + } + + if (LOCAL_STORE.getItem(oldStorageKeys.OLD_NETWORK_KEY)) { + helpers.updateNetwork(LOCAL_STORE.getItem(oldStorageKeys.OLD_NETWORK_KEY)); + LOCAL_STORE.removeItem(oldStorageKeys.OLD_NETWORK_KEY); + } + + if (LOCAL_STORE.getItem(oldStorageKeys.OLD_SERVER_KEY)) { + LOCAL_STORE.setItem( + SERVER_KEY, + LOCAL_STORE.getItem(oldStorageKeys.OLD_SERVER_KEY), + ) + LOCAL_STORE.removeItem(oldStorageKeys.OLD_SERVER_KEY); } if (LOCAL_STORE.getItem(oldStorageKeys.OLD_STARTED_KEY)) { diff --git a/src/utils/version.js b/src/utils/version.js index f2860285..159b0575 100644 --- a/src/utils/version.js +++ b/src/utils/version.js @@ -83,7 +83,7 @@ const version = { * @inner */ isLedgerCustomTokenAllowed() { - const version = LOCAL_STORE.getWalletVersion(); + const version = LOCAL_STORE.getLedgerAppVersion(); if (version !== null) return helpers.cmpVersionString(version, LEDGER_FIRST_CUSTOM_TOKEN_COMPATIBLE_VERSION) >= 0; return false; } From 38fee9ef11d7a8f25033acd4c2e3e425c72f65bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 16 Oct 2023 17:15:42 -0300 Subject: [PATCH 16/23] chore: bump version v0.27.0-rc4 (#419) --- 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 44749b9f..541fdb29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.27.0-rc3", + "version": "0.27.0-rc4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fabe1c0f..e71d9cfb 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.0-rc3", + "version": "0.27.0-rc4", "private": true, "dependencies": { "@hathor/wallet-lib": "^1.0.2", diff --git a/public/electron.js b/public/electron.js index 89bd8165..d0319336 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.0-rc3'; +const walletVersion = '0.27.0-rc4'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/constants.js b/src/constants.js index e2585451..e6a74926 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,7 +20,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.27.0-rc3'; +export const VERSION = '0.27.0-rc4'; /** * Before this version the data in localStorage from the wallet is not compatible From b12c202caa9f989a5afe5a14a57a37d010f1ac1a Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Wed, 25 Oct 2023 17:15:16 -0300 Subject: [PATCH 17/23] chore: pin action versions (#423) --- .github/workflows/main.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96be807b..57a67de3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,9 +18,11 @@ jobs: matrix: node-version: [14.x] steps: - - uses: actions/checkout@v2 + # https://github.com/actions/checkout/releases/tag/v4.0.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + # https://github.com/actions/setup-node/releases/tag/v3.8.1 + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d with: node-version: ${{ matrix.node-version }} - name: Install dependencies @@ -38,7 +40,8 @@ jobs: - name: Unit tests run: npm run test -- --coverage - name: Upload coverage - uses: codecov/codecov-action@v3 + # https://github.com/codecov/codecov-action/releases/tag/v3.1.4 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d - name: Start run: npm start & npx wait-on http://localhost:3000 env: From f208df6495cbefed1e4e344b4e147d32ecc0464f Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Wed, 25 Oct 2023 17:15:48 -0300 Subject: [PATCH 18/23] chore: pin dependency versions (#422) --- package.json | 88 ++++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index e71d9cfb..3fe26db2 100644 --- a/package.json +++ b/package.json @@ -25,34 +25,34 @@ "version": "0.27.0-rc4", "private": true, "dependencies": { - "@hathor/wallet-lib": "^1.0.2", - "@ledgerhq/hw-transport-node-hid": "^6.27.1", - "@sentry/electron": "^3.0.7", - "babel-polyfill": "^6.26.0", - "bootstrap": "^4.0.0", - "eslint-config-airbnb": "^17.1.0", - "eslint-plugin-react": "^7.13.0", - "font-awesome": "^4.7.0", - "jquery": "^3.4.1", - "npm-run-all": "^4.1.2", - "patch-package": "^6.4.7", - "popper.js": "^1.15.0", - "prop-types": "^15.7.2", - "qrcode.react": "^0.9.3", - "react": "^16.8.6", - "react-copy-to-clipboard": "^5.0.1", - "react-dom": "^16.8.6", - "react-loading": "^2.0.3", - "react-paginate": "^6.3.2", - "react-redux": "^7.1.0", - "react-router-dom": "^5.0.1", - "react-scripts": "^3.0.1", - "redux": "^4.0.0", - "redux-saga": "^1.2.1", - "redux-thunk": "^2.4.1", - "ttag": "^1.7.22", - "unleash-proxy-client": "^1.11.0", - "viz.js": "^2.1.2" + "@hathor/wallet-lib": "1.0.2", + "@ledgerhq/hw-transport-node-hid": "6.27.1", + "@sentry/electron": "3.0.7", + "babel-polyfill": "6.26.0", + "bootstrap": "4.6.1", + "eslint-config-airbnb": "17.1.1", + "eslint-plugin-react": "7.30.0", + "font-awesome": "4.7.0", + "jquery": "3.6.0", + "npm-run-all": "4.1.5", + "patch-package": "6.4.7", + "popper.js": "1.16.1", + "prop-types": "15.8.1", + "qrcode.react": "0.9.3", + "react": "16.14.0", + "react-copy-to-clipboard": "5.1.0", + "react-dom": "16.14.0", + "react-loading": "2.0.3", + "react-paginate": "6.5.0", + "react-redux": "7.2.8", + "react-router-dom": "5.3.3", + "react-scripts": "3.4.4", + "redux": "4.2.0", + "redux-saga": "1.2.1", + "redux-thunk": "2.4.1", + "ttag": "1.7.24", + "unleash-proxy-client": "1.11.0", + "viz.js": "2.1.2" }, "main": "public/electron.js", "homepage": "./", @@ -80,23 +80,23 @@ "generate-doc": "npx jsdoc -c jsdoc.json -r src/. README.md || exit 0" }, "devDependencies": { - "@sentry/browser": "^5.0.5", - "@sentry/cli": "^1.40.0", - "@testing-library/cypress": "^8.0.7", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^12.1.5", - "@testing-library/user-event": "^13.5.0", - "cypress": "^11.2.0", + "@sentry/browser": "5.30.0", + "@sentry/cli": "1.74.4", + "@testing-library/cypress": "8.0.7", + "@testing-library/jest-dom": "5.16.5", + "@testing-library/react": "12.1.5", + "@testing-library/user-event": "13.5.0", + "cypress": "11.2.0", "electron": "13.6.9", - "electron-builder": "^23.0.3", - "electron-devtools-installer": "^2.2.4", - "electron-notarize": "^0.1.1", - "eslint-plugin-cypress": "^2.12.1", - "jsdoc": "^3.6.2", - "nodemon": "^2.0.0", - "sass": "^1.52.2", - "ttag-cli": "^1.7.27", - "typescript": "^3.5.3" + "electron-builder": "23.0.3", + "electron-devtools-installer": "2.2.4", + "electron-notarize": "0.1.1", + "eslint-plugin-cypress": "2.12.1", + "jsdoc": "3.6.10", + "nodemon": "2.0.16", + "sass": "1.52.2", + "ttag-cli": "1.9.4", + "typescript": "3.9.10" }, "build": { "appId": "network.hathor.macos.wallet", From 3bbfd03733225154f8e9a0b96e3451111f12aa49 Mon Sep 17 00:00:00 2001 From: Tulio Miranda Date: Fri, 27 Oct 2023 13:39:22 -0300 Subject: [PATCH 19/23] chore: sets dependabot to dev branch (#424) --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c20e993f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: / + schedule: + interval: "weekly" + target-branch: "dev" + labels: + - "dependencies" From 69617c51bf64de4bde85cd4ed2378377e8d1b43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 31 Oct 2023 12:24:52 -0300 Subject: [PATCH 20/23] chore: fix ledger issues found during QA v0.27.0-rc4 (#420) * chore: use wallet instance to sign txs on ledger * chore: await transaction to be ready to push * feat: remove unnecessary modal show * chore: decision explanation --- src/App.js | 20 +++++++++++--------- src/components/Version.js | 19 ++++++++----------- src/constants.js | 5 ----- src/sagas/wallet.js | 13 +++++++------ src/screens/SendTokens.js | 6 +++--- src/screens/Server.js | 2 +- src/screens/Welcome.js | 9 ++------- src/storage.js | 1 + 8 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/App.js b/src/App.js index 74d66a30..a6515b13 100644 --- a/src/App.js +++ b/src/App.js @@ -41,7 +41,7 @@ import RequestErrorModal from './components/RequestError'; import store from './store/index'; import createRequestInstance from './api/axiosInstance'; import hathorLib from '@hathor/wallet-lib'; -import { IPC_RENDERER, LEDGER_ENABLED } from './constants'; +import { IPC_RENDERER } from './constants'; import AddressList from './screens/AddressList'; import NFTList from './screens/NFTList'; import { updateLedgerClosed } from './actions/index'; @@ -81,6 +81,12 @@ class Root extends React.Component { // Start the wallet as locked LOCAL_STORE.lock(); + // Ensure we have the network set even before the first ever load. + const localNetwork = LOCAL_STORE.getNetwork(); + if (!localNetwork) { + LOCAL_STORE.setNetwork('mainnet'); + } + if (IPC_RENDERER) { // Event called when user quits hathor app IPC_RENDERER.on('ledger:closed', async () => { @@ -266,13 +272,11 @@ const returnStartedRoute = (Component, props, rest) => { }}/>; } - // Wallet is not loaded nor loading, but it's started. Go to the first screen after "welcome" if (routeRequiresWalletToBeLoaded) { - return LEDGER_ENABLED - ? - : ; + // Wallet is not loaded or loading, but it is started + // Since it requires the wallet to be loaded, redirect to the wallet_type screen + return ; } - // Wallet is not loaded nor loading, and the route does not require it. // Do not redirect anywhere, just render the component. return ; @@ -308,9 +312,7 @@ const returnDefaultComponent = (Component, props) => { LOCAL_STORE.isHardwareWallet() ) { // This will redirect the page to Wallet Type screen - LOCAL_STORE.resetStorage(); - // XXX: We are skipping destroying the storage this may help - // recover the storage if the same wallet is started later + LOCAL_STORE.cleanWallet(); return ; } else { return ( diff --git a/src/components/Version.js b/src/components/Version.js index e2945b3b..42e77236 100644 --- a/src/components/Version.js +++ b/src/components/Version.js @@ -8,7 +8,7 @@ import React from 'react'; import { t } from 'ttag'; import $ from 'jquery'; -import { VERSION, LEDGER_ENABLED } from '../constants'; +import { VERSION } from '../constants'; import { GlobalModalContext, MODAL_TYPES } from './GlobalModal'; import SoftwareWalletWarningMessage from './SoftwareWalletWarningMessage'; import LOCAL_STORE from '../storage'; @@ -25,16 +25,13 @@ class Version extends React.Component { * If it's software wallet show modal warning */ walletTypeClicked = () => { - if (LEDGER_ENABLED) { - if (!LOCAL_STORE.isHardwareWallet()) { - $('#softwareWalletWarningModal').modal('show'); - this.context.showModal(MODAL_TYPES.ALERT, { - body: , - buttonName: 'Ok', - id: 'softwareWalletWarningModal', - title: 'Software wallet warning', - }); - } + if (!LOCAL_STORE.isHardwareWallet()) { + this.context.showModal(MODAL_TYPES.ALERT, { + body: , + buttonName: 'Ok', + id: 'softwareWalletWarningModal', + title: 'Software wallet warning', + }); } } diff --git a/src/constants.js b/src/constants.js index e6a74926..a269db64 100644 --- a/src/constants.js +++ b/src/constants.js @@ -131,11 +131,6 @@ if (window.require) { */ export const IPC_RENDERER = ipcRenderer; -/** - * Flag to hide/show elements after ledger integration is done - */ -export const LEDGER_ENABLED = true; - /** * Flag to hide/show create NFT button */ diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index a0a04e0a..37ff9840 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -114,12 +114,13 @@ export function* startWallet(action) { yield put(loadingAddresses(true)); yield put(storeRouterHistory(routerHistory)); - const walletId = yield LOCAL_STORE.getWalletId(); - if (!walletId) { - // The wallet has not been initialized yet - if (hardware) { - yield LOCAL_STORE.initHWStorage(xpub); - } else { + if (hardware) { + // We need to ensure that the hardware wallet storage is always generated here since we may be + // starting the wallet with a second device and so we cannot trust the xpub saved on storage. + yield LOCAL_STORE.initHWStorage(xpub); + } else { + const walletId = yield LOCAL_STORE.getWalletId(); + if (!walletId) { yield LOCAL_STORE.initStorage(words, password, pin, passphrase); } } diff --git a/src/screens/SendTokens.js b/src/screens/SendTokens.js index 82705e69..d97efa81 100644 --- a/src/screens/SendTokens.js +++ b/src/screens/SendTokens.js @@ -190,14 +190,14 @@ class SendTokens extends React.Component { /** * Add signature to each input and execute send transaction */ - onLedgerSuccess = (signatures) => { + onLedgerSuccess = async (signatures) => { try { // Prepare data and submit job to tx mining API const arr = []; for (let i=0;i { @@ -219,7 +219,7 @@ class SendTokens extends React.Component { getSignatures = () => { ledger.getSignatures( Object.assign({}, this.data), - this.props.wallet.storage, + this.props.wallet, ); } diff --git a/src/screens/Server.js b/src/screens/Server.js index 21a3aa4d..023672c7 100644 --- a/src/screens/Server.js +++ b/src/screens/Server.js @@ -232,7 +232,7 @@ class Server extends React.Component { this.props.useWalletService ? this.state.selectedWsServer : null, ); - const networkChanged = !LOCAL_STORE.getNetwork().startsWith('testnet'); + const networkChanged = !LOCAL_STORE.getNetwork()?.startsWith('testnet'); // Set network on config singleton so the load wallet will get it properly hathorLib.config.setNetwork(this.state.selectedNetwork); diff --git a/src/screens/Welcome.js b/src/screens/Welcome.js index 0871dbe0..9f4dbc62 100644 --- a/src/screens/Welcome.js +++ b/src/screens/Welcome.js @@ -12,7 +12,7 @@ import SpanFmt from '../components/SpanFmt'; import logo from '../assets/images/hathor-logo.png'; import wallet from '../utils/wallet'; import InitialImages from '../components/InitialImages'; -import { LEDGER_ENABLED, TERMS_OF_SERVICE_URL, PRIVACY_POLICY_URL } from '../constants'; +import { TERMS_OF_SERVICE_URL, PRIVACY_POLICY_URL } from '../constants'; import { str2jsx } from '../utils/i18n'; import helpers from '../utils/helpers'; import LOCAL_STORE from '../storage'; @@ -39,12 +39,7 @@ class Welcome extends React.Component { LOCAL_STORE.markWalletAsStarted(); // For the mainnet sentry will be disabled by default and the user can change this on Settings wallet.disallowSentry(); - if (LEDGER_ENABLED) { - this.props.history.push('/wallet_type/'); - } else { - LOCAL_STORE.setHardwareWallet(false); - this.props.history.push('/signin/'); - } + this.props.history.push('/wallet_type/'); } } diff --git a/src/storage.js b/src/storage.js index 3ab26c8a..02764e1c 100644 --- a/src/storage.js +++ b/src/storage.js @@ -148,6 +148,7 @@ export class LocalStorageStore { const storage = this.getStorage(); await storage.saveAccessData(accessData); this._storage = storage; + this.updateStorageVersion(); return storage; } From d551c89d5460b02e348c2e0103cf7c2b050f030d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 31 Oct 2023 12:57:36 -0300 Subject: [PATCH 21/23] chore: bump v0.27.0-rc5 (#429) --- 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 541fdb29..5816dfdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.27.0-rc4", + "version": "0.27.0-rc5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3fe26db2..46aaab81 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.0-rc4", + "version": "0.27.0-rc5", "private": true, "dependencies": { "@hathor/wallet-lib": "1.0.2", diff --git a/public/electron.js b/public/electron.js index d0319336..891df768 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.0-rc4'; +const walletVersion = '0.27.0-rc5'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/constants.js b/src/constants.js index a269db64..2e5d7371 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,7 +20,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.27.0-rc4'; +export const VERSION = '0.27.0-rc5'; /** * Before this version the data in localStorage from the wallet is not compatible From d9003a77ae106bb33a76ffff8d5e9543554929c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 8 Nov 2023 11:37:12 -0300 Subject: [PATCH 22/23] chore: show untrust all tokens option (#430) --- src/screens/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Settings.js b/src/screens/Settings.js index 76ea174a..01c91f62 100644 --- a/src/screens/Settings.js +++ b/src/screens/Settings.js @@ -315,7 +315,7 @@ class Settings extends React.Component { render() { const serverURL = this.props.useWalletService ? hathorLib.config.getWalletServiceBaseUrl() : hathorLib.config.getServerUrl(); const wsServerURL = this.props.useWalletService ? hathorLib.config.getWalletServiceBaseWsUrl() : ''; - const ledgerCustomTokens = (!LOCAL_STORE.isHardwareWallet()) && version.isLedgerCustomTokenAllowed(); + const ledgerCustomTokens = LOCAL_STORE.isHardwareWallet() && version.isLedgerCustomTokenAllowed(); const uniqueIdentifier = helpers.getUniqueId(); return ( From 648be16acbfa70a71913d8908641bdd3c61f9661 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Wed, 8 Nov 2023 21:44:17 -0300 Subject: [PATCH 23/23] chore: bump wallet to v0.27.0 (#431) --- 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 5816dfdf..053dca39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet", - "version": "0.27.0-rc5", + "version": "0.27.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 46aaab81..97982221 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.0-rc5", + "version": "0.27.0", "private": true, "dependencies": { "@hathor/wallet-lib": "1.0.2", diff --git a/public/electron.js b/public/electron.js index 891df768..7c3dad0b 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.0-rc5'; +const walletVersion = '0.27.0'; const debugMode = ( process.argv.indexOf('--unsafe-mode') >= 0 && diff --git a/src/constants.js b/src/constants.js index 2e5d7371..39c42329 100644 --- a/src/constants.js +++ b/src/constants.js @@ -20,7 +20,7 @@ export const WALLET_HISTORY_COUNT = 10; /** * Wallet version */ -export const VERSION = '0.27.0-rc5'; +export const VERSION = '0.27.0'; /** * Before this version the data in localStorage from the wallet is not compatible