Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove store usage related to ledger from wallet.js #2422

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions packages/frontend/src/components/accounts/ledger/SetupLedger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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());
Expand All @@ -140,6 +144,9 @@ const SetupLedger = (props) => {
(e) => {
setConnect('fail');
throw e;
},
() => {
dispatch(checkAndHideLedgerModal());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have an await? I see that the createAsyncThunk call that creates checkAndHideLedgerModal has synchronous logic in its payloadCreator argument but since that function is declared async it will still return a Promise. If an await were added to that payloadCreator body later on, an await would need to be added here anyway to avoid fire-and-forgetting a Promise that needed to be waited on. Even worse would be if an error were thrown from checkAndHideLedgerModal then this would potentially lead to an unhandled Promise rejection.

Alternatively we could omit createAsyncThunk for synchronous thunks if we don't think they'll be extended in the future and it doesn't introduce inconsistencies in our action handling.

}
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -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 }));
Expand All @@ -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 }));
}
Expand Down
15 changes: 0 additions & 15 deletions packages/frontend/src/redux/actions/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ export const {
getLedgerKey,
getAccountHelperWalletState,
clearFundedAccountNeedsDeposit,
getLedgerPublicKey,
setupRecoveryMessage,
deleteRecoveryMethod,
checkNearDropBalance,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -365,28 +360,18 @@ export const {
export const {
getAccessKeys,
removeAccessKey,
addLedgerAccessKey,
sendIdentityVerificationMethodCode,
disableLedger,
removeNonLedgerAccessKeys
} = createActions({
GET_ACCESS_KEYS: [wallet.getAccessKeys.bind(wallet), () => ({})],
REMOVE_ACCESS_KEY: [
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), () => ({})]
});

Expand Down
142 changes: 133 additions & 9 deletions packages/frontend/src/redux/slices/ledger/index.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can pending be truthy without being explicitly true? Just curious if the explicit true check is required here

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');
andy-haynes marked this conversation as resolved.
Show resolved Hide resolved
const client = await createLedgerU2FClient();
dispatch(handleShowLedgerModal({ show: true })).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the .unwrap() necessary here if we're not interested in the return value?

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

Expand All @@ -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()
);

Expand Down Expand Up @@ -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);
Expand All @@ -97,20 +218,20 @@ 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']);
set(state, ['modal', 'show'], !signInWithLedgerStatus && payload.show);
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']);
},
Expand All @@ -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');
});
Expand Down Expand Up @@ -174,6 +295,9 @@ export const actions = {
signInWithLedger,
checkAndHideLedgerModal,
signInWithLedgerAddAndSaveAccounts,
getLedgerPublicKey,
addLedgerAccessKey,
disableLedger,
...ledgerSlice.actions
};
export const reducer = ledgerSlice.reducer;
Expand Down
Loading