From 317fa7940e34a3165bcc7ae2a55432d8bab32182 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 2 Jun 2023 17:28:30 +0900 Subject: [PATCH] multi: Add ledger ui. --- app/actions/DaemonActions.js | 7 +- app/actions/LedgerActions.js | 12 --- app/actions/TrezorActions.js | 11 --- app/actions/WalletLoaderActions.js | 12 ++- .../SendTransactionButton.jsx | 19 ++++- .../buttons/SendTransactionButton/hooks.js | 8 +- .../shared/SendTransaction/Form.jsx | 7 +- .../SendTransaction/SendTransaction.jsx | 2 + .../shared/SendTransaction/hooks.js | 2 + .../CreateWalletPage/CreateWalletPage.jsx | 27 ++++--- .../GetStartedPage/CreateWalletPage/hooks.js | 4 +- .../CreateLedgerWalletForm.jsx | 75 +++++++++++++++++++ .../CreateLedgerWalletForm.module.css | 59 +++++++++++++++ .../CreateLedgerWalletForm/index.js | 1 + .../LedgerLoaderBarContainer.jsx | 33 ++++++++ .../LedgerLoaderBarContainer.module.css | 63 ++++++++++++++++ .../LedgerLoaderBarContainer/hooks.js | 10 +++ .../LedgerLoaderBarContainer/index.js | 1 + .../PreCreateWallet/PreCreateWallet.jsx | 55 ++++++++++++-- .../GetStartedPage/PreCreateWallet/hooks.js | 50 +++++++++++-- .../views/GetStartedPage/SetupWallet/hooks.js | 4 +- .../WalletSelection/Form/Form.jsx | 27 +++++++ .../WalletSelection/WalletSelection.jsx | 5 ++ app/components/views/GetStartedPage/hooks.js | 29 ++++--- .../GetStartedPage/messages/messages.jsx | 9 +++ .../LedgerPage/NoDevicePage/NoDevicePage.jsx | 19 +++++ .../NoDevicePage/NoDevicePage.module.css | 29 +++++++ .../views/LedgerPage/NoDevicePage/index.js | 1 + app/constants/config.js | 2 + app/hooks/useDaemonStartup.js | 25 +++++++ app/hooks/useLedger.js | 30 ++++++++ app/hooks/useSettings.js | 2 + .../docs/en/Ledger/PreCreateLedgerWallet1.md | 3 + .../docs/en/Ledger/PreCreateLedgerWallet2.md | 1 + app/i18n/docs/en/index.js | 3 + app/index.js | 5 ++ app/main_dev/externalRequests.js | 2 +- app/main_dev/ipc.js | 2 + app/reducers/index.js | 2 + app/reducers/ledger.js | 61 +++++++++++++++ app/reducers/snackbar.js | 16 ++++ app/reducers/walletLoader.js | 3 +- app/selectors.js | 12 ++- app/stateMachines/CreateWalletStateMachine.js | 13 +--- app/stateMachines/GetStartedStateMachine.js | 18 ++++- test/unit/actions/DaemonActions.spec.js | 2 +- .../views/GetStaredPage/CreateWallet.spec.js | 1 + .../GetStaredPage/PreCreateWallet.spec.js | 7 +- 48 files changed, 703 insertions(+), 88 deletions(-) create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.jsx create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.module.css create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/index.js create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.jsx create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.module.css create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/hooks.js create mode 100644 app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/index.js create mode 100644 app/components/views/LedgerPage/NoDevicePage/NoDevicePage.jsx create mode 100644 app/components/views/LedgerPage/NoDevicePage/NoDevicePage.module.css create mode 100644 app/components/views/LedgerPage/NoDevicePage/index.js create mode 100644 app/hooks/useLedger.js create mode 100644 app/i18n/docs/en/Ledger/PreCreateLedgerWallet1.md create mode 100644 app/i18n/docs/en/Ledger/PreCreateLedgerWallet2.md create mode 100644 app/reducers/ledger.js diff --git a/app/actions/DaemonActions.js b/app/actions/DaemonActions.js index 7c05e98534..b658b6123c 100644 --- a/app/actions/DaemonActions.js +++ b/app/actions/DaemonActions.js @@ -9,6 +9,7 @@ import { stopNotifcations } from "./NotificationActions"; import { saveSettings, updateStateSettingsChanged } from "./SettingsActions"; import { rescanCancel, showCantCloseModal } from "./ControlActions"; import { enableTrezor } from "./TrezorActions"; +import { enableLedger } from "./LedgerActions"; import { DEX_LOGOUT_ATTEMPT, DEX_LOGOUT_SUCCESS, @@ -309,7 +310,7 @@ export const removeWallet = (selectedWallet) => (dispatch) => { // selectedWallet = { // label: newWalletName, // value: { -// wallet: newWalletName, isWatchingOnly, isTrezor, isNew, +// wallet: newWalletName, isWatchingOnly, isTrezor, isLedger, isNew // network: isTestNet ? "testnet" : "mainnet" // } // } @@ -336,7 +337,8 @@ export const createWallet = (selectedWallet) => (dispatch, getState) => dispatch({ isWatchingOnly: selectedWallet.value.isWatchingOnly, createNewWallet: selectedWallet.value.isNew, - isTrezor: selectedWallet.value.istrezor, + isTrezor: selectedWallet.value.isTrezor, + isLedger: selectedWallet.value.isLedger, type: WALLETCREATED }); dispatch(setSelectedWallet(selectedWallet)); @@ -509,6 +511,7 @@ export const startWallet = confirmDexSeed }); selectedWallet.value.isTrezor && dispatch(enableTrezor()); + selectedWallet.value.isLedger && dispatch(enableLedger()); await dispatch(getVersionServiceAttempt()); await dispatch(openWalletAttempt("", false, selectedWallet)); return discoverAccountsComplete; diff --git a/app/actions/LedgerActions.js b/app/actions/LedgerActions.js index cb2b4288de..fd0b1c6f04 100644 --- a/app/actions/LedgerActions.js +++ b/app/actions/LedgerActions.js @@ -10,7 +10,6 @@ import { } from "./ControlActions"; import * as selectors from "selectors"; -import * as cfgConstants from "constants/config"; export const LDG_LEDGER_ENABLED = "LDG_LEDGER_ENABLED"; export const LDG_WALLET_CLOSED = "LDG_WALLET_CLOSED"; @@ -22,18 +21,7 @@ export const LDG_WALLET_CLOSED = "LDG_WALLET_CLOSED"; // enableLedger only sets a value in the config. Ledger connections are made // per action then dropped. export const enableLedger = () => (dispatch, getState) => { - const walletName = selectors.getWalletName(getState()); - - if (walletName) { - const config = wallet.getWalletCfg( - selectors.isTestNet(getState()), - walletName - ); - config.set(cfgConstants.LEDGER, true); - } - dispatch({ type: LDG_LEDGER_ENABLED }); - connect()(dispatch, getState); }; diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index b43a067f00..96e1175897 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -9,7 +9,6 @@ import { addressPath } from "helpers/trezor"; import { publishTransactionAttempt } from "./ControlActions"; -import * as cfgConstants from "constants/config"; import { MODEL1_DECRED_HOMESCREEN, MODELT_DECRED_HOMESCREEN @@ -49,16 +48,6 @@ export const TRZ_TREZOR_ENABLED = "TRZ_TREZOR_ENABLED"; // enableTrezor attepts to start a connection with connect if none exist and // connect to a trezor device. export const enableTrezor = () => (dispatch, getState) => { - const walletName = selectors.getWalletName(getState()); - - if (walletName) { - const config = wallet.getWalletCfg( - selectors.isTestNet(getState()), - walletName - ); - config.set(cfgConstants.TREZOR, true); - } - dispatch({ type: TRZ_TREZOR_ENABLED }); if (!setListeners) { diff --git a/app/actions/WalletLoaderActions.js b/app/actions/WalletLoaderActions.js index b3b6c77e7d..2a4134b8bb 100644 --- a/app/actions/WalletLoaderActions.js +++ b/app/actions/WalletLoaderActions.js @@ -18,7 +18,7 @@ import { getBestBlockHeightAttempt } from "./ClientActions"; import { WALLETREMOVED_FAILED } from "./DaemonActions"; -import { isTestNet, trezorDevice } from "selectors"; +import { isTestNet, trezorDevice, ledgerDevice } from "selectors"; import { walletrpc as api } from "middleware/walletrpc/api_pb"; import { push as pushHistory } from "connected-react-router"; import { stopNotifcations } from "./NotificationActions"; @@ -28,6 +28,7 @@ import * as cfgConstants from "constants/config"; import { RESCAN_PROGRESS } from "./ControlActions"; import { stopAccountMixer } from "./AccountMixerActions"; import { TRZ_WALLET_CLOSED } from "actions/TrezorActions"; +import { LDG_WALLET_CLOSED } from "actions/LedgerActions"; import { saveSettings, updateStateSettingsChanged } from "./SettingsActions"; const { SyncNotificationType } = api; @@ -156,7 +157,7 @@ export const CREATEWATCHONLYWALLET_FAILED = "CREATEWATCHONLYWALLET_FAILED"; export const CREATEWATCHONLYWALLET_SUCCESS = "CREATEWATCHONLYWALLET_SUCCESS"; export const createWatchOnlyWalletRequest = - (extendedPubKey, pubPass = "") => + (extendedPubKey, isLedger, isTrezor, pubPass = "") => (dispatch, getState) => new Promise((resolve, reject) => { dispatch({ type: CREATEWATCHONLYWALLET_ATTEMPT }); @@ -172,6 +173,12 @@ export const createWatchOnlyWalletRequest = } = getState(); const config = wallet.getWalletCfg(isTestNet(getState()), walletName); config.set(cfgConstants.IS_WATCH_ONLY, true); + if (isTrezor) { + config.set(cfgConstants.TREZOR, true); + } + if (isLedger) { + config.set(cfgConstants.LEDGER, true); + } config.delete(cfgConstants.DISCOVER_ACCOUNTS); wallet.setIsWatchingOnly(true); dispatch({ response: {}, type: CREATEWATCHONLYWALLET_SUCCESS }); @@ -269,6 +276,7 @@ const finalCloseWallet = () => async (dispatch, getState) => { await wallet.stopWallet(); dispatch({ type: CLOSEWALLET_SUCCESS }); if (trezorDevice(getState())) dispatch({ type: TRZ_WALLET_CLOSED }); + if (ledgerDevice(getState())) dispatch({ type: LDG_WALLET_CLOSED }); dispatch(pushHistory("/getstarted/initial")); } catch (error) { dispatch({ error, type: CLOSEWALLET_FAILED }); diff --git a/app/components/buttons/SendTransactionButton/SendTransactionButton.jsx b/app/components/buttons/SendTransactionButton/SendTransactionButton.jsx index ba2cf5b9ba..317241ca79 100644 --- a/app/components/buttons/SendTransactionButton/SendTransactionButton.jsx +++ b/app/components/buttons/SendTransactionButton/SendTransactionButton.jsx @@ -16,8 +16,10 @@ const SendTransactionButton = ({ unsignedTransaction, isSendingTransaction, isTrezor, + isLedger, onAttemptSignTransaction, - onAttemptSignTransactionTrezor + onAttemptSignTransactionTrezor, + onAttemptSignTransactionLedger } = useSendTransactionButton(); const signTransaction = (privpass) => { @@ -31,6 +33,12 @@ const SendTransactionButton = ({ onSubmit?.(); }; + const signTransactionLedger = () => { + if (disabled) return; + onAttemptSignTransactionLedger?.(unsignedTransaction); + onSubmit?.(); + }; + if (isTrezor) { return ( } ); + } else if (isLedger) { + return ( + + {buttonLabel ? buttonLabel : } + + ); } else { return ( { @@ -15,13 +17,17 @@ export function useSendTransactionButton() { }; const onAttemptSignTransactionTrezor = (rawUnsigTx, constructTxResponse) => dispatch(tza.signTransactionAttemptTrezor(rawUnsigTx, constructTxResponse)); + const onAttemptSignTransactionLedger = (rawUnsigTx) => + dispatch(ldgr.signTransactionAttemptLedger(rawUnsigTx)); return { unsignedTransaction, constructTxResponse, isSendingTransaction, isTrezor, + isLedger, onAttemptSignTransaction, - onAttemptSignTransactionTrezor + onAttemptSignTransactionTrezor, + onAttemptSignTransactionLedger }; } diff --git a/app/components/shared/SendTransaction/Form.jsx b/app/components/shared/SendTransaction/Form.jsx index 00e7b4332a..05cba321dd 100644 --- a/app/components/shared/SendTransaction/Form.jsx +++ b/app/components/shared/SendTransaction/Form.jsx @@ -22,6 +22,7 @@ const Form = ({ unsignedRawTx, isWatchingOnly, isTrezor, + isLedger, insuficientFunds, styles, hideDetails, @@ -71,7 +72,9 @@ const Form = ({ )} - {((isTrezor && isWatchingOnly) || !isWatchingOnly) && + {((isTrezor && isWatchingOnly) || + (isLedger && isWatchingOnly) || + !isWatchingOnly) && (getRunningIndicator ? ( - {unsignedRawTx && isWatchingOnly && !isTrezor && ( + {unsignedRawTx && isWatchingOnly && !isTrezor && !isLedger && ( } tx={unsignedRawTx} diff --git a/app/components/shared/SendTransaction/SendTransaction.jsx b/app/components/shared/SendTransaction/SendTransaction.jsx index 8eadf1e11f..faf7a834af 100644 --- a/app/components/shared/SendTransaction/SendTransaction.jsx +++ b/app/components/shared/SendTransaction/SendTransaction.jsx @@ -32,6 +32,7 @@ const SendTransaction = ({ totalSpent, notMixedAccounts, isTrezor, + isLedger, isWatchingOnly, isConstructingTransaction, attemptConstructTransaction, @@ -394,6 +395,7 @@ const SendTransaction = ({ willEnter, isWatchingOnly, isTrezor, + isLedger, insuficientFunds, styles, hideDetails, diff --git a/app/components/shared/SendTransaction/hooks.js b/app/components/shared/SendTransaction/hooks.js index 4a8d45eb3c..faf3c996de 100644 --- a/app/components/shared/SendTransaction/hooks.js +++ b/app/components/shared/SendTransaction/hooks.js @@ -24,6 +24,7 @@ export function useSendTransaction() { shallowEqual ); const isTrezor = useSelector(sel.isTrezor); + const isLedger = useSelector(sel.isLedger); const isWatchingOnly = useSelector(sel.isWatchingOnly); const isConstructingTransaction = useSelector(sel.isConstructingTransaction); const constructTxRequestAttempt = useSelector(sel.constructTxRequestAttempt); @@ -61,6 +62,7 @@ export function useSendTransaction() { totalSpent, notMixedAccounts, isTrezor, + isLedger, isWatchingOnly, isConstructingTransaction, attemptConstructTransaction, diff --git a/app/components/views/GetStartedPage/CreateWalletPage/CreateWalletPage.jsx b/app/components/views/GetStartedPage/CreateWalletPage/CreateWalletPage.jsx index 51f67f6245..5c61fc8b4c 100644 --- a/app/components/views/GetStartedPage/CreateWalletPage/CreateWalletPage.jsx +++ b/app/components/views/GetStartedPage/CreateWalletPage/CreateWalletPage.jsx @@ -103,18 +103,16 @@ const CreateWalletPage = ({ createWalletRef, onSendBack }) => { cancelCreateWallet ]); - const onCreateWatchOnly = useCallback(() => { - createWatchOnlyWalletRequest(walletMasterPubKey) - .then(() => sendEvent({ type: "WALLET_CREATED" })) - .catch((error) => sendEvent({ type: "ERROR", error })); - // we send a continue so we go to loading state - sendContinue(); - }, [ - createWatchOnlyWalletRequest, - sendEvent, - sendContinue, - walletMasterPubKey - ]); + const onCreateWatchOnly = useCallback( + (isLedger, isTrezor) => { + createWatchOnlyWalletRequest(walletMasterPubKey, isLedger, isTrezor) + .then(() => sendEvent({ type: "WALLET_CREATED" })) + .catch((error) => sendEvent({ type: "ERROR", error })); + // we send a continue so we go to loading state + sendContinue(); + }, + [createWatchOnlyWalletRequest, sendEvent, sendContinue, walletMasterPubKey] + ); const getStateComponent = useCallback(() => { const { mnemonic, error } = current.context; @@ -188,7 +186,8 @@ const CreateWalletPage = ({ createWalletRef, onSendBack }) => { ]); useEffect(() => { - const { isNew, walletMasterPubKey, mnemonic } = current.context; + const { isNew, walletMasterPubKey, mnemonic, isLedger, isTrezor } = + current.context; switch (current.value) { case "createWalletInit": setIsNew(isNew); @@ -222,7 +221,7 @@ const CreateWalletPage = ({ createWalletRef, onSendBack }) => { checkIsValid(); break; case "restoreWatchingOnly": - onCreateWatchOnly(); + onCreateWatchOnly(isLedger, isTrezor); break; case "finished": break; diff --git a/app/components/views/GetStartedPage/CreateWalletPage/hooks.js b/app/components/views/GetStartedPage/CreateWalletPage/hooks.js index cdcb279050..250c0528e2 100644 --- a/app/components/views/GetStartedPage/CreateWalletPage/hooks.js +++ b/app/components/views/GetStartedPage/CreateWalletPage/hooks.js @@ -21,8 +21,8 @@ export const useCreateWallet = () => { ); // TODO implement pubpass const createWatchOnlyWalletRequest = useCallback( - (extendedPubKey, pubPass = "") => - dispatch(wla.createWatchOnlyWalletRequest(extendedPubKey, pubPass)), + (extendedPubKey, isLedger, isTrezor, pubPass = "") => + dispatch(wla.createWatchOnlyWalletRequest(extendedPubKey, isLedger, isTrezor, pubPass)), [dispatch] ); const createWalletRequest = useCallback( diff --git a/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.jsx b/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.jsx new file mode 100644 index 0000000000..69ecddc9f0 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.jsx @@ -0,0 +1,75 @@ +import { FormattedMessage as T, defineMessages } from "react-intl"; +import { TextInput } from "inputs"; +import { KeyBlueButton, InvisibleButton } from "buttons"; +import styles from "./CreateLedgerWalletForm.module.css"; +import { Documentation } from "shared"; + +const messages = defineMessages({ + walletNameInputPlaceholder: { + id: "createLedgerWallet.walletNameInput.placeholder", + defaultMessage: "Choose a name for your Ledger Wallet" + }, + walletNameInputLabel: { + id: "createLedgerWallet.walletNameInput.label", + defaultMessage: "Wallet Name" + }, + messageWalletDupeNameError: { + id: "createLedgerWallet.dupeWalletName.error", + defaultMessage: "Please choose an unused wallet name" + } +}); + +const CreateLedgerWalletForm = ({ + createWallet, + hideCreateWalletForm, + newWalletName, + walletNameError, + onChangeCreateWalletName, + hasFailedAttemptName, + intl +}) => ( +
+
+ +
+
+ + + +
+ +
+ onChangeCreateWalletName(e.target.value)} + label={intl.formatMessage(messages.walletNameInputLabel)} + placeholder={intl.formatMessage(messages.walletNameInputPlaceholder)} + showErrors={hasFailedAttemptName} + className={styles.walletNameInput} + /> + +
+ + + + + + +
+
+
+); + +export default CreateLedgerWalletForm; diff --git a/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.module.css b/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.module.css new file mode 100644 index 0000000000..10040539b1 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/CreateLedgerWalletForm.module.css @@ -0,0 +1,59 @@ +.container { + width: 764px; +} + +.grid { + display: grid; + grid-template-columns: repeat(2, 30rem); + grid-column-gap: 76px; + font-size: 13px; + line-height: 17px; + color: var(--grey-7); + margin: 38px 0 32px 0; +} + +.title { + font-size: 27px; + line-height: 34px; + color: var(--grey-7); + width: 32rem; +} + +.textToggle { + font-size: 13px; + line-height: 16px; + height: 29px; + grid-column: 1; + margin-top: 20px; +} + +.textToggleChild { + padding: 6px 31px; +} + +.walletNameInput, +.walletNameInput input { + width: 279px !important; +} + +.buttonContrainer { + width: 279px; + display: flex; + justify-content: flex-end; + margin-top: 20px; +} + +.cancelBt, +.createWalletBt { + height: 44px; +} + +@media screen and (max-width: 768px) { + .container { + width: 355px; + } + + .grid { + grid-template-columns: 30rem; + } +} diff --git a/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/index.js b/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/index.js new file mode 100644 index 0000000000..b2674514b5 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/CreateLedgerWalletForm/index.js @@ -0,0 +1 @@ +export { default } from "./CreateLedgerWalletForm"; diff --git a/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.jsx b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.jsx new file mode 100644 index 0000000000..b13aa11451 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.jsx @@ -0,0 +1,33 @@ +import { FormattedMessage as T } from "react-intl"; +import styles from "./LedgerLoaderBarContainer.module.css"; +import { useLedgerLoaderBarContainer } from "./hooks"; + +const LedgerLoaderBarContainer = ({ loaderBar }) => { + const { ledgerDevice, deviceLabel } = useLedgerLoaderBarContainer(); + return ledgerDevice ? ( +
+
+ + + ) + }} + /> + + | + + + +
+
{loaderBar}
+
+ ) : null; +}; + +export default LedgerLoaderBarContainer; diff --git a/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.module.css b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.module.css new file mode 100644 index 0000000000..db06fe3a11 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/LedgerLoaderBarContainer.module.css @@ -0,0 +1,63 @@ +.loaderBarContainer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + box-shadow: 0px -4px 8px rgba(0, 0, 0, 0.1); + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + background-size: 14px; + background-repeat: no-repeat; + background-position: 25px center; + background-image: var(--tz-create-bt); +} + +.loaderBar div { + border-radius: 0; +} + +.loaderBar, +.loaderBar > div { + height: 60px; + background: linear-gradient( + 270deg, + var(--display-wallet-gradient-selected-right), + var(--input-color) + ); + color: var(--white-button-text); + padding-left: 1rem; + font-size: 1.1rem; +} + +.loaderBar > div > div:nth-of-type(1) { + grid-column: 2; + grid-row: 1; + align-items: center; + display: flex; + padding-left: 1rem; +} +.loaderBar > div > div:nth-of-type(2n) { + grid-column: 1; + grid-row: 1; +} + +.loaderBar > div > div:nth-of-type(2n) > div { + background-size: 2rem; + background-position: center; +} + +.deviceStatus { + font-size: 13px; + line-height: 16px; + padding-left: 55px; +} + +.deviceLabel { + font-weight: 600; + color: var(--main-dark-blue); +} + +.connected { + color: var(--accent-blue); +} diff --git a/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/hooks.js b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/hooks.js new file mode 100644 index 0000000000..be2089fb69 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/hooks.js @@ -0,0 +1,10 @@ +import { useSelector } from "react-redux"; +import { useDaemonStartup } from "hooks"; +import * as sel from "selectors"; + +export const useLedgerLoaderBarContainer = () => { + const { ledgerDevice } = useDaemonStartup(); + const deviceLabel = useSelector(sel.ledgerLabel); + + return { ledgerDevice, deviceLabel }; +}; diff --git a/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/index.js b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/index.js new file mode 100644 index 0000000000..0f8e9e04c6 --- /dev/null +++ b/app/components/views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer/index.js @@ -0,0 +1 @@ +export { default } from "./LedgerLoaderBarContainer"; diff --git a/app/components/views/GetStartedPage/PreCreateWallet/PreCreateWallet.jsx b/app/components/views/GetStartedPage/PreCreateWallet/PreCreateWallet.jsx index 545a208529..70261cc88d 100644 --- a/app/components/views/GetStartedPage/PreCreateWallet/PreCreateWallet.jsx +++ b/app/components/views/GetStartedPage/PreCreateWallet/PreCreateWallet.jsx @@ -1,9 +1,11 @@ import { FormattedMessage as T } from "react-intl"; import CreateWalletForm from "./CreateWalletForm"; import CreateTrezorWalletForm from "./CreateTrezorWalletForm"; +import CreateLedgerWalletForm from "./CreateLedgerWalletForm"; import { usePreCreateWallet } from "./hooks"; import { injectIntl } from "react-intl"; import NoDevicePage from "views/TrezorPage/NoDevicePage"; +import { default as NoDevicePageLedger } from "views/LedgerPage/NoDevicePage"; import styles from "./PreCreateWallet.module.css"; import { InvisibleButton } from "buttons"; @@ -15,7 +17,8 @@ const PreCreateWallet = ({ isCreateNewWallet, onShowCreateWallet, isTrezor, - creatingWallet + creatingWallet, + isLedger }) => { const { availableWallets, @@ -29,6 +32,7 @@ const PreCreateWallet = ({ walletNameError, isSPV, trezorDevice, + ledgerDevice, hideCreateWalletForm, onChangeCreateWalletName, createWallet, @@ -38,14 +42,16 @@ const PreCreateWallet = ({ toggleDisableCoinTypeUpgrades, gapLimit, setGapLimit, - connectTrezor + connectTrezor, + connectLedger } = usePreCreateWallet({ onSendContinue, onSendBack, onSendError, isCreateNewWallet, onShowCreateWallet, - isTrezor + isTrezor, + isLedger }); return isTrezor && !trezorDevice ? ( @@ -81,7 +87,45 @@ const PreCreateWallet = ({ disableCoinTypeUpgrades, toggleDisableCoinTypeUpgrades, gapLimit, - setGapLimit + setGapLimit, + isLedger + }} + /> + ) : isLedger && !ledgerDevice ? ( +
+ + + + +
+ ) : isLedger ? ( + ) : ( @@ -110,7 +154,8 @@ const PreCreateWallet = ({ disableCoinTypeUpgrades, toggleDisableCoinTypeUpgrades, gapLimit, - setGapLimit + setGapLimit, + isLedger }} /> ); diff --git a/app/components/views/GetStartedPage/PreCreateWallet/hooks.js b/app/components/views/GetStartedPage/PreCreateWallet/hooks.js index 41824abf41..029d9313db 100644 --- a/app/components/views/GetStartedPage/PreCreateWallet/hooks.js +++ b/app/components/views/GetStartedPage/PreCreateWallet/hooks.js @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from "react"; import { useDispatch } from "react-redux"; import { useDaemonStartup } from "hooks"; import * as trza from "actions/TrezorActions"; +import * as ldgr from "actions/LedgerActions"; export const usePreCreateWallet = ({ onSendContinue, @@ -9,7 +10,8 @@ export const usePreCreateWallet = ({ onSendError, isCreateNewWallet, onShowCreateWallet, - isTrezor + isTrezor, + isLedger }) => { const { isSPV, @@ -22,7 +24,12 @@ export const usePreCreateWallet = ({ trezorGetWalletCreationMasterPubKey, onCreateWallet, trezorEnable, - validateMasterPubKey + validateMasterPubKey, + ledgerDevice, + ledgerEnable, + ledgerDisable, + ledgerAlertNoConnectedDevice, + ledgerGetWalletCreationMasterPubKey } = useDaemonStartup(); const [newWalletName, setNewWalletName] = useState(""); const [isWatchingOnly, setIsWatchingOnly] = useState(false); @@ -38,8 +45,11 @@ export const usePreCreateWallet = ({ if (isTrezor) { trezorDisable(); } + if (isLedger) { + ledgerDisable(); + } onSendBack(); - }, [isTrezor, trezorDisable, onSendBack]); + }, [isTrezor, trezorDisable, onSendBack, isLedger, ledgerDisable]); const onChangeCreateWalletName = useCallback( (newWalletName) => { @@ -83,7 +93,11 @@ export const usePreCreateWallet = ({ } else { trezorDisable(); } - }, [isTrezor, trezorEnable, trezorDisable]); + if (isLedger) { + return ledgerEnable(); + } + return ledgerDisable(); + }, [isTrezor, trezorEnable, trezorDisable, isLedger, ledgerEnable, ledgerDisable]); const toggleDisableCoinTypeUpgrades = () => setDisableCoinTypeUpgrades((value) => !value); @@ -96,6 +110,7 @@ export const usePreCreateWallet = ({ wallet: newWalletName, isWatchingOnly, isTrezor: !!isTrezor, + isLedger: !!isLedger, isNew, network: isTestNet ? "testnet" : "mainnet", gapLimit, @@ -112,8 +127,10 @@ export const usePreCreateWallet = ({ return; } if (isTrezor && !trezorDevice) { - trezorAlertNoConnectedDevice(); - return; + return trezorAlertNoConnectedDevice(); + } + if (isLedger && !ledgerDevice) { + return ledgerAlertNoConnectedDevice(); } // onSendContinue action so getStartedStateMachine can go to // creatingWallet state. @@ -130,6 +147,18 @@ export const usePreCreateWallet = ({ ) ); } + if (isLedger) { + walletSelected.isWatchingOnly = true; + return ledgerGetWalletCreationMasterPubKey().then((walletMasterPubKey) => + onCreateWallet(walletSelected).then(() => + onShowCreateWallet({ + isNew, + walletMasterPubKey, + isLedger: true + }) + ) + ); + } return onCreateWallet(walletSelected) .then(() => onShowCreateWallet({ isNew, walletMasterPubKey })) @@ -138,6 +167,7 @@ export const usePreCreateWallet = ({ isCreateNewWallet, isTestNet, isTrezor, + isLedger, isWatchingOnly, masterPubKeyError, newWalletName, @@ -147,6 +177,9 @@ export const usePreCreateWallet = ({ trezorAlertNoConnectedDevice, trezorDevice, trezorGetWalletCreationMasterPubKey, + ledgerAlertNoConnectedDevice, + ledgerDevice, + ledgerGetWalletCreationMasterPubKey, walletMasterPubKey, walletNameError, gapLimit, @@ -172,6 +205,7 @@ export const usePreCreateWallet = ({ const dispatch = useDispatch(); const connectTrezor = useCallback(() => dispatch(trza.connect()), [dispatch]); + const connectLedger = useCallback(() => dispatch(ldgr.connect()), [dispatch]); return { availableWallets, @@ -194,6 +228,8 @@ export const usePreCreateWallet = ({ toggleDisableCoinTypeUpgrades, gapLimit, setGapLimit, - connectTrezor + ledgerDevice, + connectTrezor, + connectLedger }; }; diff --git a/app/components/views/GetStartedPage/SetupWallet/hooks.js b/app/components/views/GetStartedPage/SetupWallet/hooks.js index d5b77e04a9..e346850c78 100644 --- a/app/components/views/GetStartedPage/SetupWallet/hooks.js +++ b/app/components/views/GetStartedPage/SetupWallet/hooks.js @@ -87,7 +87,7 @@ export const useWalletSetup = (settingUpWalletRef) => { isCreateNewWallet, isRestoreNewWallet } = ctx; - const { isWatchingOnly, isTrezor } = selectedWallet.value; + const { isWatchingOnly, isTrezor, isLedger } = selectedWallet.value; let component, hasSoloTickets; @@ -115,7 +115,7 @@ export const useWalletSetup = (settingUpWalletRef) => { switch (current.value) { case "settingAccountsPass": { // step not needed with trezor or watching only wallets. - if (isWatchingOnly || isTrezor) { + if (isWatchingOnly || isTrezor || isLedger) { sendContinue(); return; } diff --git a/app/components/views/GetStartedPage/WalletSelection/Form/Form.jsx b/app/components/views/GetStartedPage/WalletSelection/Form/Form.jsx index b8610894f6..506a4e3977 100644 --- a/app/components/views/GetStartedPage/WalletSelection/Form/Form.jsx +++ b/app/components/views/GetStartedPage/WalletSelection/Form/Form.jsx @@ -26,6 +26,7 @@ const WalletSelectionForm = ({ availableWallets, showCreateWalletForm, showCreateTrezorBackedWalletForm, + showCreateLedgerBackedWalletForm, onRemoveWallet, onToggleEditWallet, editWallets, @@ -71,6 +72,17 @@ const WalletSelectionForm = ({ /> ); + const LedgerButton = (props) => ( + showCreateLedgerBackedWalletForm()} + text={intl.formatMessage(messages.ledgerTabMsg)} + iconColor={trezorIconColor} + {...props} + /> + ); + return (
@@ -100,6 +112,7 @@ const WalletSelectionForm = ({ +
)} + {wallet.value.isLedger && ( +
+ + }> + + +
+ )} {wallet.isWatchingOnly && (
{ + onSendCreateWallet(false, false, true); + }, [onSendCreateWallet]); + return ( diff --git a/app/components/views/GetStartedPage/hooks.js b/app/components/views/GetStartedPage/hooks.js index 6d6f6bfbc5..ade4acbaf2 100644 --- a/app/components/views/GetStartedPage/hooks.js +++ b/app/components/views/GetStartedPage/hooks.js @@ -26,6 +26,7 @@ import styles from "./GetStarted.module.css"; import { isObject } from "lodash"; import { wallet } from "wallet-preload-shim"; import TrezorLoaderBarContainer from "views/GetStartedPage/PreCreateWallet/TrezorLoaderBarContainer"; +import LedgerLoaderBarContainer from "views/GetStartedPage/PreCreateWallet/LedgerLoaderBarContainer"; import { LoaderBarContainer } from "./helpers"; export const useGetStarted = () => { @@ -164,7 +165,7 @@ export const useGetStarted = () => { isAtStartWallet: (context) => { const { selectedWallet } = context; const { passPhrase } = context; - const { isWatchingOnly, isTrezor } = selectedWallet.value; + const { isWatchingOnly, isTrezor, isLedger } = selectedWallet.value; const hasPassPhrase = !!passPhrase; onStartWallet(selectedWallet, hasPassPhrase) .then((discoverAccountsComplete) => { @@ -174,7 +175,8 @@ export const useGetStarted = () => { !discoverAccountsComplete && !passPhrase && !isWatchingOnly && - !isTrezor + !isTrezor && + !isLedger ) { // Need to discover accounts and the passphrase isn't stored in // context, so ask for the private passphrase before continuing. @@ -373,7 +375,7 @@ export const useGetStarted = () => { ); const onSendCreateWallet = useCallback( - (isNew, isTrezor) => send({ type: "CREATE_WALLET", isNew, isTrezor }), + (isNew, isTrezor, isLedger) => send({ type: "CREATE_WALLET", isNew, isTrezor, isLedger }), [send] ); @@ -409,12 +411,13 @@ export const useGetStarted = () => { ); const onShowCreateWallet = useCallback( - ({ isNew, walletMasterPubKey, isTrezor }) => + ({ isNew, walletMasterPubKey, isTrezor, isLedger }) => send({ type: "SHOW_CREATE_WALLET", isNew, walletMasterPubKey, - isTrezor + isTrezor, + isLedger }), [send] ); @@ -489,7 +492,8 @@ export const useGetStarted = () => { isTrezor, isSPV, createWalletRef, - settingUpWalletRef + settingUpWalletRef, + isLedger } = state.context; let component, text, animationType, PageComponent; @@ -560,9 +564,15 @@ export const useGetStarted = () => { m="Create a trezor wallet..." /> ); - hideHeader = isTrezor; - showLoaderBar = isTrezor; - loaderBarContainer = isTrezor ? TrezorLoaderBarContainer : null; + text = isLedger && ( + + ); + hideHeader = isTrezor || isLedger; + showLoaderBar = isTrezor || isLedger; + loaderBarContainer = isTrezor ? TrezorLoaderBarContainer : isLedger ? LedgerLoaderBarContainer : null; component = h(PreCreateWalletForm, { onShowCreateWallet, onSendContinue, @@ -570,6 +580,7 @@ export const useGetStarted = () => { onSendError, isCreateNewWallet, isTrezor, + isLedger, error }); break; diff --git a/app/components/views/GetStartedPage/messages/messages.jsx b/app/components/views/GetStartedPage/messages/messages.jsx index df9a250106..1fe4f2a005 100644 --- a/app/components/views/GetStartedPage/messages/messages.jsx +++ b/app/components/views/GetStartedPage/messages/messages.jsx @@ -81,6 +81,10 @@ export const messages = defineMessages({ id: "getStarted.trezor", defaultMessage: "Setup a Trezor Wallet" }, + ledgerTabMsg: { + id: "getStarted.ledger", + defaultMessage: "Setup a Ledger Wallet" + }, closeEditWallets: { id: "getStarted.closeEditWallets", defaultMessage: "Close" @@ -108,6 +112,11 @@ export const messages = defineMessages({ defaultMessage: "Trezor is a hardware wallet. For more information, visit {link}" }, + messageWalletLedgerDescription: { + id: "createwallet.ledger.description", + defaultMessage: + "Ledger is a hardware wallet. For more information, visit {link}" + }, messageWalletMasterPubKey: { id: "createwallet.walletpubkey.placeholder", defaultMessage: "Master Pub Key" diff --git a/app/components/views/LedgerPage/NoDevicePage/NoDevicePage.jsx b/app/components/views/LedgerPage/NoDevicePage/NoDevicePage.jsx new file mode 100644 index 0000000000..194ce9a8f4 --- /dev/null +++ b/app/components/views/LedgerPage/NoDevicePage/NoDevicePage.jsx @@ -0,0 +1,19 @@ +import styles from "./NoDevicePage.module.css"; +import { FormattedMessage as T } from "react-intl"; +import { KeyBlueButton } from "buttons"; + +const NoDevicePage = ({ onConnect }) => ( +
+
+ +
+ + + +
+); + +export default NoDevicePage; diff --git a/app/components/views/LedgerPage/NoDevicePage/NoDevicePage.module.css b/app/components/views/LedgerPage/NoDevicePage/NoDevicePage.module.css new file mode 100644 index 0000000000..ad8d45215e --- /dev/null +++ b/app/components/views/LedgerPage/NoDevicePage/NoDevicePage.module.css @@ -0,0 +1,29 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +.desc { + font-size: 27px; + line-height: 34px; + color: var(--grey-7); + width: 522px; + text-align: center; + margin-bottom: 40px; +} + +.button { + height: 29px; + font-size: 13px; + line-height: 16.34px; + padding: 6px 10px; +} + +@media screen and (max-width: 768px) { + .desc { + width: 80%; + font-size: 22px; + line-height: 23px; + } +} diff --git a/app/components/views/LedgerPage/NoDevicePage/index.js b/app/components/views/LedgerPage/NoDevicePage/index.js new file mode 100644 index 0000000000..ef97d62181 --- /dev/null +++ b/app/components/views/LedgerPage/NoDevicePage/index.js @@ -0,0 +1 @@ +export { default } from "./NoDevicePage"; diff --git a/app/constants/config.js b/app/constants/config.js index d700935986..4f1df9200c 100644 --- a/app/constants/config.js +++ b/app/constants/config.js @@ -59,6 +59,7 @@ export const IS_WATCH_ONLY = "iswatchonly"; export const POLITEIA_LAST_ACCESS_TIME = "politeia_last_access_time"; export const POLITEIA_LAST_ACCESS_BLOCK = "politeia_last_access_block"; export const TREZOR = "trezor"; +export const LEDGER = "ledger"; export const ENABLE_PRIVACY = "enableprivacy"; export const LN_ACCOUNT = "ln_account"; export const LN_ADDRESS = "ln_address"; @@ -98,6 +99,7 @@ export const WALLET_INITIAL_VALUE = { [POLITEIA_LAST_ACCESS_TIME]: 0, [POLITEIA_LAST_ACCESS_BLOCK]: 0, [TREZOR]: false, + [LEDGER]: false, // enable_privacy only shows the privacy menu on the wallet [ENABLE_PRIVACY]: true, [LN_ACCOUNT]: null, diff --git a/app/hooks/useDaemonStartup.js b/app/hooks/useDaemonStartup.js index 676aea1f0f..20f2588321 100644 --- a/app/hooks/useDaemonStartup.js +++ b/app/hooks/useDaemonStartup.js @@ -6,6 +6,7 @@ import * as da from "actions/DaemonActions"; import * as ca from "actions/ClientActions"; import * as ctrla from "actions/ControlActions"; import * as trza from "actions/TrezorActions"; +import * as ldgr from "actions/LedgerActions"; import * as ama from "actions/AccountMixerActions"; import * as va from "actions/VSPActions"; @@ -38,7 +39,9 @@ const useDaemonStartup = () => { const getDaemonStarted = useSelector(sel.getDaemonStarted); const getEstimatedTimeLeft = useSelector(sel.getEstimatedTimeLeft); const trezorDevice = useSelector(sel.trezorDevice); + const ledgerDevice = useSelector(sel.ledgerDevice); const isTrezor = useSelector(sel.isTrezor); + const isLedger = useSelector(sel.isLedger); const syncAttemptRequest = useSelector(sel.getSyncAttemptRequest); const daemonWarning = useSelector(sel.daemonWarning); const isSettingAccountsPassphrase = useSelector( @@ -230,6 +233,22 @@ const useDaemonStartup = () => { () => dispatch(trza.getWalletCreationMasterPubKey()), [dispatch] ); + const ledgerEnable = useCallback( + () => dispatch(ldgr.enableLedger()), + [dispatch] + ); + const ledgerDisable = useCallback( + () => dispatch(ldgr.disableLedger()), + [dispatch] + ); + const ledgerAlertNoConnectedDevice = useCallback( + () => dispatch(ldgr.alertNoConnectedDevice()), + [dispatch] + ); + const ledgerGetWalletCreationMasterPubKey = useCallback( + () => dispatch(ldgr.getWalletCreationMasterPubKey()), + [dispatch] + ); const validateMasterPubKey = useCallback( (masterPubKey) => dispatch(ctrla.validateMasterPubKey(masterPubKey)), [dispatch] @@ -279,6 +298,11 @@ const useDaemonStartup = () => { trezorDisable, trezorEnable, trezorLoadDeviceList, + ledgerGetWalletCreationMasterPubKey, + ledgerAlertNoConnectedDevice, + ledgerDisable, + ledgerEnable, + ledgerDevice, getDcrwalletLogs, onCreateWallet, goToErrorPage, @@ -325,6 +349,7 @@ const useDaemonStartup = () => { getEstimatedTimeLeft, trezorDevice, isTrezor, + isLedger, peerCount, synced, syncFetchMissingCfiltersAttempt, diff --git a/app/hooks/useLedger.js b/app/hooks/useLedger.js new file mode 100644 index 0000000000..0f5abeb950 --- /dev/null +++ b/app/hooks/useLedger.js @@ -0,0 +1,30 @@ +import { useSelector, useDispatch } from "react-redux"; +import { useCallback } from "react"; +import * as sel from "selectors"; +import * as ldgr from "actions/LedgerActions"; + +const useLedger = () => { + const isLedger = useSelector(sel.isLedger); + const device = useSelector(sel.ledgerDevice); + const walletCreationMasterPubkeyAttempt = useSelector( + sel.ledgerWalletCreationMasterPubkeyAttempt + ); + + const dispatch = useDispatch(); + + const onConnect = useCallback(() => dispatch(ldgr.connect()), [dispatch]); + const onEnableLedger = useCallback( + () => dispatch(ldgr.enableLedger()), + [dispatch] + ); + + return { + isLedger, + device, + walletCreationMasterPubkeyAttempt, + onConnect, + onEnableLedger + }; +}; + +export default useLedger; diff --git a/app/hooks/useSettings.js b/app/hooks/useSettings.js index 7a09c2b75e..4167becfc1 100644 --- a/app/hooks/useSettings.js +++ b/app/hooks/useSettings.js @@ -24,6 +24,7 @@ const useSettings = () => { const walletName = useSelector(sel.getWalletName); const walletReady = useSelector(sel.getWalletReady); const isTrezor = useSelector(sel.isTrezor); + const isLedger = useSelector(sel.isLedger); const onAttemptChangePassphrase = useCallback( (oldPass, args) => { @@ -124,6 +125,7 @@ const useSettings = () => { walletName, walletReady, isTrezor, + isLedger, onAttemptChangePassphrase, onChangeTempSettings, onSaveSettings, diff --git a/app/i18n/docs/en/Ledger/PreCreateLedgerWallet1.md b/app/i18n/docs/en/Ledger/PreCreateLedgerWallet1.md new file mode 100644 index 0000000000..e2719b812c --- /dev/null +++ b/app/i18n/docs/en/Ledger/PreCreateLedgerWallet1.md @@ -0,0 +1,3 @@ +You must use an already setup Ledger “Device Wallet”, a device already initialized with a seed and the decred app installed. New devices should be initialized via [ledger live](https://www.ledger.com/ledger-live/) and the decred app downloaded and installed there. + +A Ledger-backed wallet can be used with multiple cryptocurrencies without conflict. In other words, if you already use your device wallet with other coins, you can keep using it and use the same seed for your Decrediton wallet. diff --git a/app/i18n/docs/en/Ledger/PreCreateLedgerWallet2.md b/app/i18n/docs/en/Ledger/PreCreateLedgerWallet2.md new file mode 100644 index 0000000000..a65e8d55df --- /dev/null +++ b/app/i18n/docs/en/Ledger/PreCreateLedgerWallet2.md @@ -0,0 +1 @@ +Ledger wallets used within Decrediton do not currently support staking operations (purchasing, voting and revoking tickets) or signing messages. diff --git a/app/i18n/docs/en/index.js b/app/i18n/docs/en/index.js index 674ab06ac2..0a2d1a1ebc 100644 --- a/app/i18n/docs/en/index.js +++ b/app/i18n/docs/en/index.js @@ -93,3 +93,6 @@ export { default as TrezorRecoverDevice } from "./Trezor/RecoverDevice.md"; export { default as TrezorWipeDevice } from "./Trezor/WipeDevice.md"; export { default as PreCreateTrezorWallet1 } from "./Trezor/PreCreateTrezorWallet1.md"; export { default as PreCreateTrezorWallet2 } from "./Trezor/PreCreateTrezorWallet2.md"; + +export { default as PreCreateLedgerWallet1 } from "./Ledger/PreCreateLedgerWallet1.md"; +export { default as PreCreateLedgerWallet2 } from "./Ledger/PreCreateLedgerWallet2.md"; diff --git a/app/index.js b/app/index.js index 48abab17fc..20493999a4 100644 --- a/app/index.js +++ b/app/index.js @@ -406,6 +406,11 @@ const initialState = { performingTogglePassphraseOnDeviceProtection: false, deviceLabel: undefined }, + ledger: { + enabled: false, + device: false, + walletCreationMasterPubkeyAttempt: false + }, ln: { enabled: globalCfg.get(cfgConstants.LN_ENABLED), active: false, diff --git a/app/main_dev/externalRequests.js b/app/main_dev/externalRequests.js index 0eef75a5b9..a87463a7d5 100644 --- a/app/main_dev/externalRequests.js +++ b/app/main_dev/externalRequests.js @@ -78,7 +78,7 @@ export const installSessionHandlers = (mainLogger) => { ); callback({ cancel: true, requestHeaders: details.requestHeaders }); } else { - //logger.log("verbose", details.method + " " + details.url); + // logger.log("verbose", details.method + " " + details.url); if ( allowedExternalRequests[EXTERNALREQUEST_TREZOR_BRIDGE] && /^http:\/\/127.0.0.1:21325\//.test(details.url) diff --git a/app/main_dev/ipc.js b/app/main_dev/ipc.js index 204968eb5e..7f0c5f1b5b 100644 --- a/app/main_dev/ipc.js +++ b/app/main_dev/ipc.js @@ -56,6 +56,7 @@ export const getAvailableWallets = (network) => { const lastAccess = cfg.get(cfgConstants.LAST_ACCESS); const isWatchingOnly = cfg.get(cfgConstants.IS_WATCH_ONLY); const isTrezor = cfg.get(cfgConstants.TREZOR); + const isLedger = cfg.get(cfgConstants.LEDGER); const isPrivacy = cfg.get(cfgConstants.MIXED_ACCOUNT_CFG); const walletDbFilePath = getWalletDb(isTestNet, wallet); const finished = fs.existsSync(walletDbFilePath); @@ -68,6 +69,7 @@ export const getAvailableWallets = (network) => { lastAccess, isWatchingOnly, isTrezor, + isLedger, isPrivacy, isLN, displayWalletGradient diff --git a/app/reducers/index.js b/app/reducers/index.js index e0f50f4f35..51c57de385 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -12,6 +12,7 @@ import snackbar from "./snackbar"; import statistics from "./statistics"; import governance from "./governance"; import trezor from "./trezor"; +import ledger from "./ledger"; import ln from "./ln"; import vsp from "./vsp"; import dex from "./dex"; @@ -30,6 +31,7 @@ export default { statistics, governance, trezor, + ledger, ln, vsp, dex diff --git a/app/reducers/ledger.js b/app/reducers/ledger.js new file mode 100644 index 0000000000..db47290a5b --- /dev/null +++ b/app/reducers/ledger.js @@ -0,0 +1,61 @@ +import { + LDG_WALLET_CLOSED, + LDG_LEDGER_ENABLED, + LDG_LEDGER_DISABLED, + LDG_CONNECT_ATTEMPT, + LDG_CONNECT_FAILED, + LDG_CONNECT_SUCCESS, + LDG_NOCONNECTEDDEVICE, + LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT, + LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED, + LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS +} from "actions/LedgerActions"; +import { CLOSEWALLET_SUCCESS } from "actions/WalletLoaderActions"; + +export default function ledger(state = {}, action) { + switch (action.type) { + case LDG_LEDGER_ENABLED: + return { ...state, enabled: true }; + case LDG_LEDGER_DISABLED: + return { ...state, enabled: false }; + case LDG_CONNECT_ATTEMPT: + return { + ...state, + connectAttempt: true + }; + case LDG_CONNECT_SUCCESS: + return { + ...state, + // Ledger does not keep a constant connection. Device is set to true on + // the first successful attempt and left true until the wallet is closed. + device: true, + connectAttempt: false + }; + case LDG_CONNECT_FAILED: + return { + ...state, + connectError: action.error, + connectAttempt: false + }; + case LDG_NOCONNECTEDDEVICE: + // We don't currently listen for reconnect so not deleting the device. + return { + ...state + }; + case LDG_WALLET_CLOSED: + return { + ...state, + device: false, + connected: false + }; + case LDG_GETWALLETCREATIONMASTERPUBKEY_ATTEMPT: + return { ...state, walletCreationMasterPubkeyAttempt: true }; + case LDG_GETWALLETCREATIONMASTERPUBKEY_SUCCESS: + case LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED: + return { ...state, walletCreationMasterPubkeyAttempt: false }; + case CLOSEWALLET_SUCCESS: + return { ...state, enabled: false }; + default: + return state; + } +} diff --git a/app/reducers/snackbar.js b/app/reducers/snackbar.js index fb0f74682e..287a318786 100644 --- a/app/reducers/snackbar.js +++ b/app/reducers/snackbar.js @@ -105,6 +105,10 @@ import { TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED, TRZ_NOTBACKEDUP } from "actions/TrezorActions"; +import { + LDG_NOCONNECTEDDEVICE, + LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED +} from "actions/LedgerActions"; import { NEW_TRANSACTIONS_RECEIVED, TRANSACTION_TYPES, @@ -362,6 +366,16 @@ const messages = defineMessages({ defaultMessage: "Trezor must be backed up in order to perform this operation." }, + LDG_NOCONNECTEDDEVICE: { + id: "ledger.noConnectedDevice", + defaultMessage: + "No Ledger device connected. Check the device connection and Ledger bridge." + }, + LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED: { + id: "ledger.getWalletCreationMasterPubKey.failed", + defaultMessage: + "Failed to obtain master extended pubkey from Ledger device: {originalError}" + }, ERROR_IS_OBJECT: { id: "snackbar.errorObject", defaultMessage: "The following error happened: {error}" @@ -839,6 +853,8 @@ export default function snackbar(state = {}, action) { case TRZ_NOTBACKEDUP: case TRZ_BACKUPDEVICE_FAILED: case TRZ_GETWALLETCREATIONMASTERPUBKEY_FAILED: + case LDG_NOCONNECTEDDEVICE: + case LDG_GETWALLETCREATIONMASTERPUBKEY_FAILED: case LNWALLET_CREATEACCOUNT_FAILED: case LNWALLET_STARTDCRLND_FAILED: case LNWALLET_CONNECT_FAILED: diff --git a/app/reducers/walletLoader.js b/app/reducers/walletLoader.js index b566505ee6..304f3fee1c 100644 --- a/app/reducers/walletLoader.js +++ b/app/reducers/walletLoader.js @@ -71,7 +71,8 @@ export default function walletLoader(state = {}, action) { return { ...state, isWatchingOnly: action.isWatchingOnly, - isTrezor: action.isTrezor + isTrezor: action.isTrezor, + isLedger: action.isLedger }; case WALLET_SELECTED: return { ...state, selectedWallet: action.selectedWallet }; diff --git a/app/selectors.js b/app/selectors.js index 8a287b5591..2058cfc398 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -1104,9 +1104,11 @@ export const confirmationDialogModalVisible = bool( export const isTrezor = get(["trezor", "enabled"]); export const isPerformingTrezorUpdate = get(["trezor", "performingUpdate"]); +export const isLedger = get(["ledger", "enabled"]); + export const isSignMessageDisabled = and(isWatchingOnly, not(isTrezor)); export const isChangePassPhraseDisabled = isWatchingOnly; -export const isTransactionsSendTabDisabled = not(isTrezor); +export const isTransactionsSendTabDisabled = bool(and(not(isTrezor), not(isLedger))); export const politeiaURL = createSelector([isTestNet], (isTestNet) => isTestNet ? POLITEIA_URL_TESTNET : POLITEIA_URL_MAINNET @@ -1313,6 +1315,12 @@ export const trezorWalletCreationMasterPubkeyAttempt = get([ "walletCreationMasterPubkeyAttempt" ]); +export const ledgerDevice = get(["ledger", "device"]); +export const ledgerWalletCreationMasterPubkeyAttempt = get([ + "ledger", + "walletCreationMasterPubkeyAttempt" +]); + // selectors for checking if decrediton can be closed. // getRunningIndicator is a indicator for indicate something is runnning on // decrediton, like the ticket auto buyer or the mixer. @@ -1329,7 +1337,7 @@ export const loggedInDex = bool(get(["dex", "loggedIn"])); // ln selectors -export const lnEnabled = bool(and(not(isWatchingOnly), not(isTrezor))); +export const lnEnabled = bool(and(not(isWatchingOnly), not(isTrezor), not(isLedger))); export const lnActive = bool(get(["ln", "active"])); export const lnStartupStage = get(["ln", "startupStage"]); export const lnStartAttempt = bool(get(["ln", "startAttempt"])); diff --git a/app/stateMachines/CreateWalletStateMachine.js b/app/stateMachines/CreateWalletStateMachine.js index 380b2423f9..cd73379106 100644 --- a/app/stateMachines/CreateWalletStateMachine.js +++ b/app/stateMachines/CreateWalletStateMachine.js @@ -13,7 +13,9 @@ export const CreateWalletMachine = Machine({ mnemonic: "", seed: "", passPhrase: "", - walletMasterPubKey: "" + walletMasterPubKey: "", + isTrezor: false, + isLedger: false }, states: { createWalletInit: { @@ -26,10 +28,6 @@ export const CreateWalletMachine = Machine({ target: "restoreWatchingOnly", cond: (c, event) => event.isWatchingOnly }, - RESTORE_TREZOR_WALLET: { - target: "restoreTrezor", - cond: (c, event) => event.isTrezor - }, RESTORE_WALLET: { target: "writeSeed", cond: (c, event) => event.isRestore @@ -124,11 +122,6 @@ export const CreateWalletMachine = Machine({ CONTINUE: "loading" } }, - restoreTrezor: { - on: { - CONTINUE: "loading" - } - }, loading: { on: { ERROR: { diff --git a/app/stateMachines/GetStartedStateMachine.js b/app/stateMachines/GetStartedStateMachine.js index 075c7c268e..0c100aae19 100644 --- a/app/stateMachines/GetStartedStateMachine.js +++ b/app/stateMachines/GetStartedStateMachine.js @@ -26,6 +26,7 @@ export const getStartedMachine = Machine({ initial: "preStart", on: { SHOW_TREZOR_CONFIG: "trezorConfig", + SHOW_LEDGER_CONFIG: "ledgerConfig", SHOW_CREATE_WALLET: "creatingWallet", SHOW_SETTING_UP_WALLET: "settingUpWallet" }, @@ -213,7 +214,8 @@ export const getStartedMachine = Machine({ !isUndefined(event.isNew) ? !event.isNew : context.isRestoreNewWallet, - isTrezor: (context, event) => event.isTrezor + isTrezor: (context, event) => event.isTrezor, + isLedger: (context, event) => event.isLedger }) }, ERROR: { @@ -319,7 +321,8 @@ export const getStartedMachine = Machine({ CreateWalletMachine.withContext({ isNew: e.isNew, walletMasterPubKey: e.walletMasterPubKey, - isTrezor: e.isTrezor + isTrezor: e.isTrezor, + isLedger: e.isLedger }) ); } catch (e) { @@ -373,6 +376,7 @@ export const getStartedMachine = Machine({ isCreateNewWallet: ctx.isCreateNewWallet, isWatchingOnly: ctx.selectedWallet.isWatchingOnly, isTrezor: ctx.selectedWallet.isTrezor, + isLedger: ctx.selectedWallet.isLedger, passPhrase: ctx.passPhrase }) ); @@ -394,6 +398,16 @@ export const getStartedMachine = Machine({ BACK: "startMachine.hist", SHOW_TREZOR_CONFIG: "trezorConfig" } + }, + ledgerConfig: { + initial: "ledgerConfig", + states: { + ledgerConfig: {} + }, + on: { + BACK: "startMachine.hist", + SHOW_LEDGER_CONFIG: "ledgerConfig" + } } } }); diff --git a/test/unit/actions/DaemonActions.spec.js b/test/unit/actions/DaemonActions.spec.js index 971a8ca48f..5b416fb893 100644 --- a/test/unit/actions/DaemonActions.spec.js +++ b/test/unit/actions/DaemonActions.spec.js @@ -883,7 +883,7 @@ test("test createWallet", async () => { selectedWallet.value.isWatchingOnly ); expect(store.getState().walletLoader.isTrezor).toBe( - selectedWallet.value.istrezor + selectedWallet.value.isTrezor ); expect(mockWalletCfgSet).toHaveBeenCalledWith( diff --git a/test/unit/components/views/GetStaredPage/CreateWallet.spec.js b/test/unit/components/views/GetStaredPage/CreateWallet.spec.js index ba88768eda..0135854aba 100644 --- a/test/unit/components/views/GetStaredPage/CreateWallet.spec.js +++ b/test/unit/components/views/GetStaredPage/CreateWallet.spec.js @@ -24,6 +24,7 @@ const testSelectedWallet = { value: { isNew: true, isTrezor: false, + isLedger: false, isWatchingOnly: false, network: "mainnet", wallet: testWalletName, diff --git a/test/unit/components/views/GetStaredPage/PreCreateWallet.spec.js b/test/unit/components/views/GetStaredPage/PreCreateWallet.spec.js index 47df248fac..3a68a3cb69 100644 --- a/test/unit/components/views/GetStaredPage/PreCreateWallet.spec.js +++ b/test/unit/components/views/GetStaredPage/PreCreateWallet.spec.js @@ -19,6 +19,7 @@ const testSelectedWallet = { value: { isNew: true, isTrezor: false, + isLedger: false, isWatchingOnly: false, network: "mainnet", wallet: testWalletName, @@ -258,6 +259,8 @@ test("test watch only control on restore wallet", async () => { await wait(() => expect(mockCreateWatchOnlyWalletRequest).toHaveBeenCalledWith( testValidMasterPubKey, + undefined, + undefined, "" ) ); @@ -286,7 +289,7 @@ test("test create trezor-backed wallet page (trezor device is connected)", async }); const testRestoreSelectedWallet = { ...testSelectedWallet, - value: { ...testSelectedWallet.value, isNew: false, isTrezor: true }, + value: { ...testSelectedWallet.value, isNew: false, isTrezor: true, isLedger: false }, isWatchingOnly: true }; @@ -338,6 +341,8 @@ test("test create trezor-backed wallet page (trezor device is connected)", async await wait(() => expect(mockCreateWatchOnlyWalletRequest).toHaveBeenCalledWith( testWalletCreationMasterPubKey, + undefined, + true, "" ) );