diff --git a/packages/frontend/src/components/accounts/ledger/SetupLedger.js b/packages/frontend/src/components/accounts/ledger/SetupLedger.js index e0694eacf5..a2255ee423 100644 --- a/packages/frontend/src/components/accounts/ledger/SetupLedger.js +++ b/packages/frontend/src/components/accounts/ledger/SetupLedger.js @@ -5,17 +5,16 @@ import { connect } from 'react-redux'; import { DISABLE_CREATE_ACCOUNT, RECAPTCHA_CHALLENGE_API_KEY } from '../../../config'; import { Mixpanel } from '../../../mixpanel/index'; import { - addLedgerAccessKey, createNewAccount, refreshAccount, redirectToApp, redirectTo, checkIsNew, - fundCreateAccountLedger, - getLedgerPublicKey + fundCreateAccountLedger } from '../../../redux/actions/account'; import { showCustomAlert } from '../../../redux/actions/status'; import { selectAccountSlice } from '../../../redux/slices/account'; +import { actions as ledgerActions } from '../../../redux/slices/ledger'; import { actions as linkdropActions } from '../../../redux/slices/linkdrop'; import { selectStatusMainLoader } from '../../../redux/slices/status'; import parseFundingOptions from '../../../utils/parseFundingOptions'; @@ -27,6 +26,12 @@ import { isRetryableRecaptchaError, Recaptcha } from '../../Recaptcha'; import LedgerIcon from '../../svg/LedgerIcon'; import InstructionsModal from './InstructionsModal'; +const { + addLedgerAccessKey, + checkAndHideLedgerModal, + getLedgerPublicKey +} = ledgerActions; + const { setLinkdropAmount } = linkdropActions; // FIXME: Use `debug` npm package so we can keep some debug logging around but not spam the console everywhere @@ -73,9 +78,8 @@ const SetupLedger = (props) => { let publicKey; try { - debugLog(DISABLE_CREATE_ACCOUNT, fundingOptions); - publicKey = await dispatch(getLedgerPublicKey()); + publicKey = await dispatch(getLedgerPublicKey()).unwrap(); await setKeyMeta(publicKey, { type: 'ledger' }); Mixpanel.track("SR-Ledger Set key meta"); @@ -125,7 +129,7 @@ const SetupLedger = (props) => { return; } } else { - await dispatch(addLedgerAccessKey()); + await dispatch(addLedgerAccessKey()).unwrap(); Mixpanel.track("SR-Ledger Add ledger access key"); } await dispatch(refreshAccount()); @@ -140,6 +144,9 @@ const SetupLedger = (props) => { (e) => { setConnect('fail'); throw e; + }, + () => { + dispatch(checkAndHideLedgerModal()); } ); }; diff --git a/packages/frontend/src/components/profile/hardware_devices/HardwareDevices.js b/packages/frontend/src/components/profile/hardware_devices/HardwareDevices.js index 1b27e77faf..e8285fe8cd 100644 --- a/packages/frontend/src/components/profile/hardware_devices/HardwareDevices.js +++ b/packages/frontend/src/components/profile/hardware_devices/HardwareDevices.js @@ -5,20 +5,22 @@ import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; import { Mixpanel } from '../../../mixpanel/index'; -import { - getAccessKeys, - disableLedger, - getLedgerKey, - addLedgerAccessKey -} from '../../../redux/actions/account'; +import { getAccessKeys, getLedgerKey } from '../../../redux/actions/account'; import selectRecoveryLoader from '../../../redux/crossStateSelectors/selectRecoveryLoader'; import { selectAccountSlice } from '../../../redux/slices/account'; +import { actions as ledgerActions } from '../../../redux/slices/ledger'; import { actions as recoveryMethodsActions } from '../../../redux/slices/recoveryMethods'; import FormButton from '../../common/FormButton'; import SkeletonLoading from '../../common/SkeletonLoading'; import Card from '../../common/styled/Card.css'; import ConfirmDisable from './ConfirmDisable'; +const { + addLedgerAccessKey, + checkAndHideLedgerModal, + disableLedger +} = ledgerActions; + const { fetchRecoveryMethods } = recoveryMethodsActions; const Container = styled(Card)` @@ -82,10 +84,11 @@ const HardwareDevices = ({ recoveryMethods }) => { await Mixpanel.withTracking("SR-Ledger Handle confirm disable", async () => { setDisabling(true); - await dispatch(disableLedger()); + await dispatch(disableLedger()).unwrap(); }, () => {}, async () => { + dispatch(checkAndHideLedgerModal()); await dispatch(getAccessKeys()); await dispatch(getLedgerKey()); await dispatch(fetchRecoveryMethods({ accountId: account.accountId })); @@ -98,7 +101,13 @@ const HardwareDevices = ({ recoveryMethods }) => { const handleConnectLedger = async () => { await Mixpanel.withTracking("SR-Ledger Reconnect ledger", async () => { - await dispatch(addLedgerAccessKey()); + try { + await dispatch(addLedgerAccessKey()).unwrap(); + } catch (error) { + throw error; + } finally { + dispatch(checkAndHideLedgerModal()); + } await dispatch(getLedgerKey()); await dispatch(fetchRecoveryMethods({ accountId: account.accountId })); } diff --git a/packages/frontend/src/redux/actions/account.js b/packages/frontend/src/redux/actions/account.js index 8195377b8e..ea2e43994f 100644 --- a/packages/frontend/src/redux/actions/account.js +++ b/packages/frontend/src/redux/actions/account.js @@ -243,7 +243,6 @@ export const { getLedgerKey, getAccountHelperWalletState, clearFundedAccountNeedsDeposit, - getLedgerPublicKey, setupRecoveryMessage, deleteRecoveryMethod, checkNearDropBalance, @@ -325,10 +324,6 @@ export const { wallet.clearFundedAccountNeedsDeposit.bind(wallet), () => showAlert({ onlyError: true }) ], - GET_LEDGER_PUBLIC_KEY: [ - wallet.getLedgerPublicKey.bind(wallet), - () => ({}) - ], SETUP_RECOVERY_MESSAGE: [ wallet.setupRecoveryMessage.bind(wallet), () => showAlert() @@ -365,9 +360,7 @@ export const { export const { getAccessKeys, removeAccessKey, - addLedgerAccessKey, sendIdentityVerificationMethodCode, - disableLedger, removeNonLedgerAccessKeys } = createActions({ GET_ACCESS_KEYS: [wallet.getAccessKeys.bind(wallet), () => ({})], @@ -375,18 +368,10 @@ export const { wallet.removeAccessKey.bind(wallet), () => showAlert({ onlyError: true }) ], - ADD_LEDGER_ACCESS_KEY: [ - wallet.addLedgerAccessKey.bind(wallet), - () => showAlert({ onlyError: true }) - ], SEND_IDENTITY_VERIFICATION_METHOD_CODE: [ wallet.sendIdentityVerificationMethodCode.bind(wallet), () => showAlert({ localAlert: true }) ], - DISABLE_LEDGER: [ - wallet.disableLedger.bind(wallet), - () => ({}) - ], REMOVE_NON_LEDGER_ACCESS_KEYS: [wallet.removeNonLedgerAccessKeys.bind(wallet), () => ({})] }); diff --git a/packages/frontend/src/redux/slices/ledger/index.js b/packages/frontend/src/redux/slices/ledger/index.js index 87c3a28b6d..297c27abaf 100644 --- a/packages/frontend/src/redux/slices/ledger/index.js +++ b/packages/frontend/src/redux/slices/ledger/index.js @@ -1,13 +1,25 @@ import { createAsyncThunk, createSlice, current } from "@reduxjs/toolkit"; import set from 'lodash.set'; import unset from 'lodash.unset'; +import { KeyPair } from "near-api-js"; +import { PublicKey } from "near-api-js/lib/utils"; +import { KeyType } from "near-api-js/lib/utils/key_pair"; import { createSelector } from "reselect"; import { HIDE_SIGN_IN_WITH_LEDGER_ENTER_ACCOUNT_ID_MODAL } from "../../../config"; +import * as Config from '../../../config'; import { showAlertToolkit } from "../../../utils/alerts"; +import { getAccountIds } from "../../../utils/helper-api"; import { setLedgerHdPath } from "../../../utils/localStorage"; -import { wallet } from "../../../utils/wallet"; +import { setKeyMeta, wallet } from "../../../utils/wallet"; +import { WalletError } from "../../../utils/walletError"; +import { makeAccountActive } from "../../actions/account"; import refreshAccountOwner from "../../sharedThunks/refreshAccountOwner"; +import { selectStatusActionStatus } from '../status'; + +const { + NETWORK_ID +} = Config; const SLICE_NAME = 'ledger'; @@ -21,9 +33,102 @@ const initialState = { modal: {} }; +const handleShowLedgerModal = createAsyncThunk( + `${SLICE_NAME}/handleShowLedgerModal`, + async ({ show }, { dispatch, getState }) => { + const actionStatus = selectStatusActionStatus(getState()); + const actions = Object.keys(actionStatus).filter((action) => actionStatus[action]?.pending === true); + const action = actions.length ? actions[actions.length - 1] : false; + dispatch(ledgerSlice.actions.showLedgerModal({ show, action })); + } +); + +const getLedgerPublicKey = createAsyncThunk( + `${SLICE_NAME}/getLedgerPublicKey`, + async ({ path } = {}, { dispatch }) => { + const { createLedgerU2FClient } = await import('../../../utils/ledger.js'); + const client = await createLedgerU2FClient(); + dispatch(handleShowLedgerModal({ show: true })).unwrap(); + const rawPublicKey = await client.getPublicKey(path); + return new PublicKey({ keyType: KeyType.ED25519, data: rawPublicKey }); + } +); + +const addLedgerAccessKey = createAsyncThunk( + `${SLICE_NAME}/addLedgerAccessKey`, + async (_, { dispatch }) => { + const accountId = wallet.accountId; + const ledgerPublicKey = await dispatch(getLedgerPublicKey()).unwrap(); + const accessKeys = await wallet.getAccessKeys(); + const accountHasLedgerKey = accessKeys.map(key => key.public_key).includes(ledgerPublicKey.toString()); + await setKeyMeta(ledgerPublicKey, { type: 'ledger' }); + + const account = await wallet.getAccount(accountId); + if (!accountHasLedgerKey) { + await account.addKey(ledgerPublicKey); + await wallet.postSignedJson('/account/ledgerKeyAdded', { accountId, publicKey: ledgerPublicKey.toString() }); + } + }, + showAlertToolkit({ onlyError: true }) +); + +const disableLedger = createAsyncThunk( + `${SLICE_NAME}/disableLedger`, + async (_, { dispatch }) => { + const account = await wallet.getAccount(wallet.accountId); + const keyPair = KeyPair.fromRandom('ed25519'); + await account.addKey(keyPair.publicKey); + await wallet.keyStore.setKey(NETWORK_ID, wallet.accountId, keyPair); + + const path = localStorage.getItem(`ledgerHdPath:${wallet.accountId}`); + const publicKey = await dispatch(getLedgerPublicKey({ path })).unwrap(); + await wallet.removeAccessKey(publicKey); + await wallet.getAccessKeys(wallet.accountId); + + await wallet.deleteRecoveryMethod({ kind: 'ledger', publicKey: publicKey.toString() }); + localStorage.removeItem(`ledgerHdPath:${wallet.accountId}`); + } +); + const getLedgerAccountIds = createAsyncThunk( `${SLICE_NAME}/getLedgerAccountIds`, - async ({ path }) => await wallet.getLedgerAccountIds({ path }), + async ({ path }, { dispatch }) => { + const publicKey = await dispatch(getLedgerPublicKey({ path })).unwrap(); + + // TODO: getXXX methods shouldn't be modifying the state + await setKeyMeta(publicKey, { type: 'ledger' }); + + let accountIds; + try { + accountIds = await getAccountIds(publicKey); + } catch (error) { + if (error.name === 'AbortError') { + throw new WalletError('Fetch aborted.', 'getLedgerAccountIds.aborted'); + } + throw error; + } + + const checkedAccountIds = (await Promise.all( + accountIds.map(async (accountId) => { + try { + const accountKeys = await (await wallet.getAccount(accountId)).getAccessKeys(); + return accountKeys.find(({ public_key }) => public_key === publicKey.toString()) ? accountId : null; + } catch (error) { + if (error.toString().indexOf('does not exist while viewing') !== -1) { + return null; + } + throw error; + } + }) + )) + .filter(accountId => accountId); + + if (!checkedAccountIds.length) { + throw new WalletError('No accounts were found.', 'getLedgerAccountIds.noAccounts'); + } + + return checkedAccountIds; + }, showAlertToolkit({ onlyError: true }) ); @@ -35,7 +140,23 @@ const addLedgerAccountId = createAsyncThunk( const saveAndSelectLedgerAccounts = createAsyncThunk( `${SLICE_NAME}/saveAndSelectLedgerAccounts`, - async ({ accounts}) => await wallet.saveAndSelectLedgerAccounts({ accounts }), + async ({ accounts}, { dispatch }) => { + const accountIds = Object.keys(accounts).filter(accountId => accounts[accountId].status === 'success'); + + if (!accountIds.length) { + throw new WalletError('No accounts were accepted.', 'getLedgerAccountIds.noAccountsAccepted'); + } + + await Promise.all(accountIds.map(async (accountId) => { + await wallet.saveAccount(accountId); + })); + + dispatch(makeAccountActive(accountIds[accountIds.length - 1])); + + return { + numberOfAccounts: accountIds.length + }; + }, showAlertToolkit() ); @@ -82,7 +203,7 @@ const ledgerSlice = createSlice({ name: SLICE_NAME, initialState, reducers: { - setLedgerTxSigned(state, { payload, ready, error }) { + setLedgerTxSigned(state, { payload }) { const { signInWithLedger } = current(state); set(state, ['txSigned'], payload.status); @@ -97,12 +218,12 @@ const ledgerSlice = createSlice({ set(state, ['signInWithLedger', payload.accountId, 'status'], 'pending'); } }, - clearSignInWithLedgerModalState(state, { payload, ready, error }) { + clearSignInWithLedgerModalState(state) { unset(state, ['txSigned']); unset(state, ['signInWithLedgerStatus']); unset(state, ['signInWithLedger']); }, - showLedgerModal(state, { payload, ready, error }) { + showLedgerModal(state, { payload }) { const { signInWithLedgerStatus } = current(state); unset(state, ['txSigned']); @@ -110,7 +231,7 @@ const ledgerSlice = createSlice({ set(state, ['modal', 'action'], payload.action); set(state, ['modal', 'textId'], 'ledgerSignTxModal.DEFAULT'); }, - hideLedgerModal(state, { payload, ready, error }) { + hideLedgerModal(state) { set(state, ['modal'], {}); unset(state, ['txSigned']); }, @@ -135,11 +256,11 @@ const ledgerSlice = createSlice({ unset(state, ['txSigned']); }); // addLedgerAccountId - builder.addCase(addLedgerAccountId.pending, (state, { payload, meta: { arg: { accountId } } }) => { + builder.addCase(addLedgerAccountId.pending, (state, { meta: { arg: { accountId } } }) => { set(state, ['signInWithLedgerStatus'], LEDGER_MODAL_STATUS.CONFIRM_ACCOUNTS); set(state, ['signInWithLedger', accountId, 'status'], 'confirm'); }); - builder.addCase(addLedgerAccountId.fulfilled, (state, { payload, meta: { arg: { accountId } } }) => { + builder.addCase(addLedgerAccountId.fulfilled, (state, { meta: { arg: { accountId } } }) => { set(state, ['signInWithLedgerStatus'], LEDGER_MODAL_STATUS.CONFIRM_ACCOUNTS); set(state, ['signInWithLedger', accountId, 'status'], 'success'); }); @@ -174,6 +295,9 @@ export const actions = { signInWithLedger, checkAndHideLedgerModal, signInWithLedgerAddAndSaveAccounts, + getLedgerPublicKey, + addLedgerAccessKey, + disableLedger, ...ledgerSlice.actions }; export const reducer = ledgerSlice.reducer; diff --git a/packages/frontend/src/routes/SetupLedgerNewAccountWrapper.js b/packages/frontend/src/routes/SetupLedgerNewAccountWrapper.js index 927c468f66..d89720c385 100644 --- a/packages/frontend/src/routes/SetupLedgerNewAccountWrapper.js +++ b/packages/frontend/src/routes/SetupLedgerNewAccountWrapper.js @@ -2,17 +2,30 @@ import React from 'react'; import { useDispatch } from 'react-redux'; import SetupLedgerNewAccount from '../components/accounts/ledger/SetupLedgerNewAccount'; -import { getLedgerPublicKey, redirectTo } from '../redux/actions/account'; +import { redirectTo } from '../redux/actions/account'; import { showCustomAlert } from '../redux/actions/status'; +import { actions as ledgerActions } from '../redux/slices/ledger'; import { setKeyMeta, wallet } from '../utils/wallet'; +const { + checkAndHideLedgerModal, + getLedgerPublicKey +} = ledgerActions; + export function SetupLedgerNewAccountWrapper() { const dispatch = useDispatch(); return ( { try { - const ledgerPublicKey = await dispatch(getLedgerPublicKey()); + let ledgerPublicKey; + try { + ledgerPublicKey = await dispatch(getLedgerPublicKey()).unwrap(); + } catch(error) { + throw error; + } finally { + dispatch(checkAndHideLedgerModal()); + } const implicitAccountId = Buffer.from(ledgerPublicKey.data).toString('hex'); const account = wallet.getAccountBasic(implicitAccountId); try { diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index 68ac1d3e5a..b99f20668a 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -626,35 +626,6 @@ class Wallet { } } - async addLedgerAccessKey() { - const accountId = this.accountId; - const ledgerPublicKey = await this.getLedgerPublicKey(); - const accessKeys = await this.getAccessKeys(); - const accountHasLedgerKey = accessKeys.map(key => key.public_key).includes(ledgerPublicKey.toString()); - await setKeyMeta(ledgerPublicKey, { type: 'ledger' }); - - const account = await this.getAccount(accountId); - if (!accountHasLedgerKey) { - await account.addKey(ledgerPublicKey); - await this.postSignedJson('/account/ledgerKeyAdded', { accountId, publicKey: ledgerPublicKey.toString() }); - } - } - - async disableLedger() { - const account = await this.getAccount(this.accountId); - const keyPair = KeyPair.fromRandom('ed25519'); - await account.addKey(keyPair.publicKey); - await this.keyStore.setKey(NETWORK_ID, this.accountId, keyPair); - - const path = await localStorage.getItem(`ledgerHdPath:${this.accountId}`); - const publicKey = await this.getLedgerPublicKey(path); - await this.removeAccessKey(publicKey); - await this.getAccessKeys(this.accountId); - - await this.deleteRecoveryMethod({ kind: 'ledger', publicKey: publicKey.toString() }); - await localStorage.removeItem(`ledgerHdPath:${this.accountId}`); - } - async addWalletMetadataAccessKeyIfNeeded(accountId, localAccessKey) { if (!localAccessKey || (!localAccessKey.access_key.permission.FunctionCall || !localAccessKey.access_key.permission.FunctionCall.method_names.includes(WALLET_METADATA_METHOD))) { @@ -675,46 +646,6 @@ class Wallet { return null; } - async getLedgerAccountIds({ path }) { - const publicKey = await this.getLedgerPublicKey(path); - - // TODO: getXXX methods shouldn't be modifying the state - await setKeyMeta(publicKey, { type: 'ledger' }); - - let accountIds; - try { - accountIds = await getAccountIds(publicKey); - } catch (error) { - if (error.name === 'AbortError') { - throw new WalletError('Fetch aborted.', 'getLedgerAccountIds.aborted'); - } - throw error; - } - - const checkedAccountIds = (await Promise.all( - accountIds - .map(async (accountId) => { - try { - const accountKeys = await (await this.getAccount(accountId)).getAccessKeys(); - return accountKeys.find(({ public_key }) => public_key === publicKey.toString()) ? accountId : null; - } catch (error) { - if (error.toString().indexOf('does not exist while viewing') !== -1) { - return null; - } - throw error; - } - }) - ) - ) - .filter(accountId => accountId); - - if (!checkedAccountIds.length) { - throw new WalletError('No accounts were found.', 'getLedgerAccountIds.noAccounts'); - } - - return checkedAccountIds; - } - async addLedgerAccountId({ accountId }) { try { const accessKeys = await this.getAccessKeys(accountId); @@ -730,32 +661,6 @@ class Wallet { } } - async saveAndSelectLedgerAccounts({ accounts }) { - const accountIds = Object.keys(accounts).filter(accountId => accounts[accountId].status === 'success'); - - if (!accountIds.length) { - throw new WalletError('No accounts were accepted.', 'getLedgerAccountIds.noAccountsAccepted'); - } - - await Promise.all(accountIds.map(async (accountId) => { - await this.saveAccount(accountId); - })); - - store.dispatch(makeAccountActive(accountIds[accountIds.length - 1])); - - return { - numberOfAccounts: accountIds.length - }; - } - - async getLedgerPublicKey(path) { - const { createLedgerU2FClient } = await import('./ledger.js'); - const client = await createLedgerU2FClient(); - this.dispatchShowLedgerModal(true); - const rawPublicKey = await client.getPublicKey(path); - return new PublicKey({ keyType: KeyType.ED25519, data: rawPublicKey }); - } - async getAvailableKeys() { const availableKeys = [(await this.keyStore.getKey(NETWORK_ID, this.accountId)).publicKey]; const ledgerKey = await this.getLedgerKey(this.accountId);