Skip to content

Commit

Permalink
feat: multiple wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
alexruzenhack committed Jul 18, 2024
1 parent 30c9933 commit 6ced2d3
Show file tree
Hide file tree
Showing 22 changed files with 888 additions and 31 deletions.
2 changes: 1 addition & 1 deletion locale/texts.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2768,4 +2768,4 @@ msgstr ""

#: src/components/atomic-swap/ProposalBalanceTable.js:62
msgid "Receiving"
msgstr ""
msgstr ""
56 changes: 51 additions & 5 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import SentryPermission from './screens/SentryPermission';
import UnknownTokens from './screens/UnknownTokens';
import Signin from './screens/Signin';
import LockedWallet from './screens/LockedWallet';
import ChooseWallet from './screens/ChooseWallet';
import NewWallet from './screens/NewWallet';
import WalletType from './screens/WalletType';
import SoftwareWalletWarning from './screens/SoftwareWalletWarning';
Expand All @@ -45,8 +46,9 @@ import RequestErrorModal from './components/RequestError';
import { GlobalModalContext, MODAL_TYPES } from './components/GlobalModal';
import createRequestInstance from './api/axiosInstance';
import hathorLib from '@hathor/wallet-lib';
import { IPC_RENDERER } from './constants';
import AllAddresses from './screens/AllAddresses';
import AddressList from './screens/AddressList';
import { IPC_RENDERER, LEDGER_ENABLED } from './constants';
import NFTList from './screens/NFTList';
import { resetNavigateTo, updateLedgerClosed } from './actions/index';
import { WALLET_STATUS } from './sagas/wallet';
Expand Down Expand Up @@ -80,7 +82,17 @@ function Root() {
useEffect(() => {
if (ledgerClosed) {
LOCAL_STORE.lock();
navigate('/locked/');
wallet.removeHardwareWalletFromStorage();

// If there are other wallets, go to screen to choose wallet
const firstWallet = wallet.getFirstWalletPrefix();
if (firstWallet) {
wallet.setWalletPrefix(firstWallet);
navigate('/choose_wallet');
} else {
wallet.setWalletPrefix(null);
navigate('/wallet_type/');
}
}
}, [ledgerClosed]);

Expand All @@ -91,6 +103,22 @@ function Root() {
* - Ledger event handlers
*/
useEffect(() => {
// When Ledger device loses connection or the app is closed
if (this.props.ledgerClosed && !prevProps.ledgerClosed) {
wallet.removeHardwareWalletFromStorage();

// If there are other wallets, go to screen to choose wallet
const firstWallet = wallet.getFirstWalletPrefix();
if (firstWallet) {
wallet.setWalletPrefix(firstWallet);
this.props.history.push('/choose_wallet');
} else {
wallet.setWalletPrefix(null);
this.props.history.push('/wallet_type/');
}
}

// ---
// Detect a previous instalation and migrate
storageUtils.migratePreviousLocalStorage();

Expand Down Expand Up @@ -215,6 +243,7 @@ function Root() {
<Route path="/signin" element={<StartedComponent children={ <Signin />} loaded={false} />} />
<Route path="/hardware_wallet" element={<StartedComponent children={ <StartHardwareWallet /> } loaded={false} />} />
<Route path="/locked" element={<DefaultComponent children={<LockedWallet />} />} />
<Route path="/choose_wallet" element={<DefaultComponent children={<ChooseWallet />} />} />
<Route path="/welcome" element={<Welcome />} />
<Route path="/loading_addresses" element={<LoadingAddresses />} />
<Route path="/permission" element={<SentryPermission />} />
Expand All @@ -233,7 +262,7 @@ function LoadedWalletComponent({ children }) {

// If was closed and is loaded we need to redirect to locked screen
if ((!isServerScreen) && (LOCAL_STORE.wasClosed() || LOCAL_STORE.isLocked()) && (!LOCAL_STORE.isHardwareWallet())) {
return <Navigate to={ '/locked/' } />;
return <Redirect to={{ pathname: '/choose_wallet/' }} />;
}

// We allow server screen to be shown from locked screen to allow the user to
Expand Down Expand Up @@ -303,18 +332,29 @@ function StartedComponent({children, loaded: routeRequiresWalletToBeLoaded}) {
const isServerScreen = location.pathname === '/server';
// Wallet is locked, go to locked screen
if (LOCAL_STORE.isLocked() && !isServerScreen && !LOCAL_STORE.isHardwareWallet()) {
return <Navigate to={ '/locked/' } replace />;
return <Redirect to={{pathname: '/choose_wallet/'}}/>;
}

// Route requires the wallet to be loaded, render it
if (routeRequiresWalletToBeLoaded || isServerScreen) {
return <LoadedWalletComponent children={children} />;
}

// XXX: Wallet reseted?
const firstWallet = wallet.getFirstWalletPrefix();
if (firstWallet && LOCAL_STORE.prefix === '') {
return <Redirect to={{pathname: '/choose_wallet/'}}/>;
}

// Route does not require wallet to be loaded. Redirect to wallet "home" screen
return <Navigate to="/wallet/" replace />;
}

const firstWallet = wallet.getFirstWalletPrefix();
if (firstWallet && hathorLib.storage.store.prefix === '') {
return <Redirect to={{pathname: '/choose_wallet/'}}/>;
}

// Wallet is not loaded, but it is still loading addresses. Go to the loading screen
if (loadingAddresses) {
const location = useLocation();
Expand Down Expand Up @@ -378,7 +418,13 @@ function DefaultComponent({ children }) {
LOCAL_STORE.isHardwareWallet()) {
// This will redirect the page to Wallet Type screen
LOCAL_STORE.cleanWallet();
return <Navigate to={ '/wallet_type/' } />;
wallet.removeHardwareWalletFromStorage();
hathorLib.wallet.unlock();
const firstWallet = wallet.getFirstWalletPrefix();
if (firstWallet) {
return <Redirect to={{ pathname: '/choose_wallet/' }} />;
}
return <Redirect to={{ pathname: '/wallet_type/' }} />;
}

// Render the navigation top bar, the component and the error handling modal
Expand Down
7 changes: 6 additions & 1 deletion src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ 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 });

/**
* Set the wallet name during start process
*/
export const setInitWalletName = name => ({type: "set_init_wallet_name", payload: name});

/**
* Partially update history and balance
Expand Down
5 changes: 5 additions & 0 deletions src/components/ModalResetAllData.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class ModalResetAllData extends React.Component {
componentDidMount() {
$('#confirmResetModal').modal('show');
$('#confirmResetModal').on('hidden.bs.modal', this.props.onClose);
// when the modal closes, make sures the fields are reset
// XXX: Check if hide and hidden expressions are redundant
$('#confirmResetModal').on('hide.bs.modal', () => {
this.onDismiss();
});
}

componentWillUnmount() {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { t } from 'ttag';
import logo from '../assets/images/hathor-white-logo.png';
import Version from './Version';
import ServerStatus from './ServerStatus';
import WalletStatus from './WalletStatus';
import helpers from '../utils/helpers';
import { useSelector } from 'react-redux';
import { FEATURE_TOGGLE_DEFAULTS, NANO_CONTRACTS_FEATURE_TOGGLE } from '../constants';
Expand Down Expand Up @@ -73,6 +74,7 @@ function Navigation() {
</li>}
</ul>
<div className="navbar-right d-flex flex-row align-items-center navigation-search">
<WalletStatus />
<ServerStatus />
<Version />
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/components/TokenBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,11 @@ export default function TokenBar () {
*/
const lockWallet = () => {
LOCAL_STORE.lock();
navigate('/locked/');
};
if (hathorLib.wallet.isHardwareWallet()) {
wallet.removeHardwareWalletFromStorage();
}
navigate('/choose_wallet/');
}

/**
* Called when user clicks to go to settings, then redirects to settings screen
Expand Down
2 changes: 1 addition & 1 deletion src/components/TokenHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class TokenHistory extends React.Component {
* firstHash {string} ID of the first transaction being shown
* lastHash {string} ID of the last transaction being shown
* reference {string} ID of the reference transaction used when clicking on pagination button
* direction {string} 'previous' or 'next', dependending on which pagination button the user has clicked
* direction {string} 'previous' or 'next', depending on which pagination button the user has clicked
* transactions {Array} List of transactions to be shown in the screen
* shouldFetch {Boolean} If should fetch more history (when the fetch returns 0 elements, should be set to false)
*/
Expand Down
32 changes: 32 additions & 0 deletions src/components/WalletStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* 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 React from 'react';
import { t } from 'ttag'

import hathorLib from '@hathor/wallet-lib';

/**
* Component that shows the current wallet name in use
*
* @memberof Components
*/
function WalletStatus(props) {
if (hathorLib.wallet.isHardwareWallet()) {
return null;
}
const listOfWallets = hathorLib.storage.store.getListOfWallets();
const walletName = listOfWallets[hathorLib.storage.store.prefix].name;
return (
<div className="d-flex flex-column version-wrapper align-items-center">
<span>{t`Current Wallet`}</span>
<span>{walletName}</span>
</div>
);
}

export default WalletStatus;
6 changes: 5 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ export const LEDGER_MAX_VERSION = '2.0.0';
*/
export const LEDGER_FIRST_CUSTOM_TOKEN_COMPATIBLE_VERSION = '1.1.0'


/**
* Wallet service URLs
*/
Expand Down Expand Up @@ -286,3 +285,8 @@ export const colors = {
* when changing the address of a nano contract
*/
export const NANO_UPDATE_ADDRESS_LIST_COUNT = 5;

/**
* Hardware wallet name on storage
*/
export const HARDWARE_WALLET_NAME = '$HARDWARE%'
27 changes: 27 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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.
*/

export class WalletDoesNotExistError extends Error {
constructor(prefix) {
super(`Wallet does not exist: ${prefix}`);
this.name = "WalletDoesNotExistError";
}
};

export class WalletAlreadyExistError extends Error {
constructor() {
super('Prefix already in use');
this.name = "WalletAlreadyExistError";
}
};

export class InvalidWalletNameError extends Error {
constructor() {
super('Invalid wallet name');
this.name = "InvalidWalletNameError";
}
};
90 changes: 90 additions & 0 deletions src/featureFlags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import events from 'events';
import { UnleashClient } from 'unleash-proxy-client';
import {
UNLEASH_URL,
UNLEASH_CLIENT_KEY,
UNLEASH_POLLING_INTERVAL,
} from './constants';

const IGNORE_WALLET_SERVICE_FLAG = 'featureFlags:ignoreWalletServiceFlag';

export class FeatureFlags extends events.EventEmitter {
constructor(userId, network) {
super();

this.userId = userId;
this.network = network;
this.walletServiceFlag = `wallet-service-wallet-desktop-${this.network}.rollout`;
this.walletServiceEnabled = 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);

// 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('wallet-service-enabled', walletServiceEnabled);
}
});
}

/**
* 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 });

// 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 clearIgnoreWalletServiceFlag() {
localStorage.removeItem(IGNORE_WALLET_SERVICE_FLAG);
}
}
Loading

0 comments on commit 6ced2d3

Please sign in to comment.