showCreateLedgerBackedWalletForm()}
+ text={intl.formatMessage(messages.ledgerTabMsg)}
+ iconColor={ledgerIconColor}
+ {...props}
+ />
+ );
+
return (
@@ -100,6 +112,7 @@ const WalletSelectionForm = ({
+
)}
+ {wallet.value.isLedger && (
+
+
+ }>
+
+
+
+ )}
{wallet.isWatchingOnly && (
+
>
)}
diff --git a/app/components/views/GetStartedPage/WalletSelection/WalletSelection.jsx b/app/components/views/GetStartedPage/WalletSelection/WalletSelection.jsx
index e4861af67a..a1a2751109 100644
--- a/app/components/views/GetStartedPage/WalletSelection/WalletSelection.jsx
+++ b/app/components/views/GetStartedPage/WalletSelection/WalletSelection.jsx
@@ -30,6 +30,10 @@ const WalletSelectionBody = ({
onSendCreateWallet(false, true);
}, [onSendCreateWallet]);
+ const showCreateLedgerBackedWalletForm = useCallback(() => {
+ onSendCreateWallet(false, false, true);
+ }, [onSendCreateWallet]);
+
return (
diff --git a/app/components/views/GetStartedPage/hooks.js b/app/components/views/GetStartedPage/hooks.js
index 6d6f6bfbc5..00bcd4dbc6 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,8 @@ 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 +412,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 +493,8 @@ export const useGetStarted = () => {
isTrezor,
isSPV,
createWalletRef,
- settingUpWalletRef
+ settingUpWalletRef,
+ isLedger
} = state.context;
let component, text, animationType, PageComponent;
@@ -560,9 +565,19 @@ 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 +585,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/HomePage/HomePage.jsx b/app/components/views/HomePage/HomePage.jsx
index 0406db5189..2bfb2ad465 100644
--- a/app/components/views/HomePage/HomePage.jsx
+++ b/app/components/views/HomePage/HomePage.jsx
@@ -56,8 +56,9 @@ export default () => {
// TODO: Enable ticket purchacing for Trezor.
const isTrezor = useSelector(sel.isTrezor);
+ const isLedger = useSelector(sel.isLedger);
let recentTickets, tabs;
- if (isTrezor) {
+ if (isTrezor || isLedger) {
tabs = [balanceTab, transactionsTab];
} else {
recentTickets = (
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/components/views/PrivacyPage/PrivacyPage.jsx b/app/components/views/PrivacyPage/PrivacyPage.jsx
index 78739f7463..bd66a5ad1b 100644
--- a/app/components/views/PrivacyPage/PrivacyPage.jsx
+++ b/app/components/views/PrivacyPage/PrivacyPage.jsx
@@ -1,10 +1,12 @@
import { TabbedPage, TitleHeader, DescriptionHeader } from "layout";
import { FormattedMessage as T } from "react-intl";
+import { useSelector } from "react-redux";
import SecurityTab from "./SecurityTab";
import PrivacyTab from "./PrivacyTab";
import { usePrivacyPage } from "./hooks";
import styles from "./PrivacyPage.module.css";
import { SECURITY_ICON } from "constants";
+import * as sel from "selectors";
export const PrivacyTabHeader = () => {
const { mixedAccountName, changeAccountName } = usePrivacyPage();
@@ -46,21 +48,27 @@ const PrivacyPageHeader = () => (
const PrivacyPage = () => {
const { privacyEnabled } = usePrivacyPage();
- const tabs = [
- {
- path: "/privacy/mixing",
- content: PrivacyTab,
- header: PrivacyTabHeader,
- label: ,
- disabled: !privacyEnabled
- },
- {
- path: "/privacy/security",
- content: SecurityTab,
- header: PrivacyTabHeader,
- label:
- }
- ];
+ const isTrezor = useSelector(sel.isTrezor);
+ const mixingTab = {
+ path: "/privacy/mixing",
+ content: PrivacyTab,
+ header: PrivacyTabHeader,
+ label: ,
+ disabled: !privacyEnabled
+ };
+ const securityTab = {
+ path: "/privacy/security",
+ content: SecurityTab,
+ header: PrivacyTabHeader,
+ label:
+ };
+ let tabs;
+ // Cannot currently mix with trezor and ledger hides this tab altogether.
+ if (isTrezor) {
+ tabs = [securityTab];
+ } else {
+ tabs = [mixingTab, securityTab];
+ }
return } tabs={tabs} />;
};
diff --git a/app/components/views/TransactionsPage/ReceiveTab/ReceivePage/ReceivePage.jsx b/app/components/views/TransactionsPage/ReceiveTab/ReceivePage/ReceivePage.jsx
index 48d28f37d5..045fe9e2f7 100644
--- a/app/components/views/TransactionsPage/ReceiveTab/ReceivePage/ReceivePage.jsx
+++ b/app/components/views/TransactionsPage/ReceiveTab/ReceivePage/ReceivePage.jsx
@@ -54,8 +54,9 @@ const ReceivePage = ({
// TODO: Enable ticket purchacing for Trezor.
const isTrezor = useSelector(sel.isTrezor);
+ const isLedger = useSelector(sel.isLedger);
let hardwareWalletWarning;
- if (isTrezor) {
+ if (isTrezor || isLedger) {
const warningStr =
"Caution! Hardware wallets cannot spend from special/staking inputs. " +
"Only use this address for receiving funds from normal transacitons. Do not " +
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/helpers/ledger.js b/app/helpers/ledger.js
index 3948b27e89..c04452f17a 100644
--- a/app/helpers/ledger.js
+++ b/app/helpers/ledger.js
@@ -14,7 +14,16 @@ export function addressPath(branch, index) {
// fixPubKeyChecksum replaces the sha256 checksum, or last four bytes, of a
// pubkey with a blake256 checksum.
-export function fixPubKeyChecksum(pubKey) {
+export function fixPubKeyChecksum(pubKey, isTestnet) {
+ const mainnetPubPrefix = "dpub";
+ const testnetPubPrefix = "tpub";
+ if (isTestnet) {
+ if (pubKey.slice(0, 4) !== testnetPubPrefix) {
+ throw "pubkey is not for testnet";
+ }
+ } else if (pubKey.slice(0, 4) !== mainnetPubPrefix) {
+ throw "pubkey is not for mainnet";
+ }
const buff = bs58.decode(pubKey).slice(0, -4);
const firstPass = blake("blake256").update(Buffer.from(buff)).digest();
const secondPass = blake("blake256").update(firstPass).digest();
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 d203c295dd..1fc827f2da 100644
--- a/app/i18n/docs/en/index.js
+++ b/app/i18n/docs/en/index.js
@@ -94,3 +94,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..6c1b1e0aca 100644
--- a/app/selectors.js
+++ b/app/selectors.js
@@ -1104,9 +1104,13 @@ 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 +1317,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 +1339,9 @@ 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/package.json b/package.json
index 7a92245444..2a0b26da71 100644
--- a/package.json
+++ b/package.json
@@ -274,7 +274,7 @@
"@grpc/grpc-js": "1.7.3",
"@hot-loader/react-dom": "16.14.0",
"@ledgerhq/hw-app-btc": "^10.0.5",
- "@ledgerhq/hw-transport-webusb": "^6.27.16",
+ "@ledgerhq/hw-transport-webusb": "6.27.1",
"@peculiar/webcrypto": "1.4.3",
"@xstate/react": "^0.8.1",
"blake-hash": "^2.0.0",
@@ -297,7 +297,7 @@
"minimist": "1.2.8",
"mv": "^2.1.1",
"node-polyfill-webpack-plugin": "1.1.4",
- "pi-ui": "https://github.com/decred/pi-ui/",
+ "pi-ui": "https://github.com/decred/pi-ui#4daf214b901aa9f50547cf5645594cb76e7c9e51",
"postcss": "8.4.24",
"postcss-loader": "^7.0.1",
"postcss-preset-env": "7.8.3",
@@ -343,5 +343,8 @@
"engines": {
"node": ">=14.16",
"yarn": "^1.22"
+ },
+ "resolutions": {
+ "@ledgerhq/hw-transport": "6.27.1"
}
}
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,
""
)
);
diff --git a/test/unit/helpers/ledger.spec.js b/test/unit/helpers/ledger.spec.js
index 4a9d2ff655..0b49b6312f 100644
--- a/test/unit/helpers/ledger.spec.js
+++ b/test/unit/helpers/ledger.spec.js
@@ -6,9 +6,9 @@ test("test ledger address path", () => {
test("test ledger pubkey to hd key", () => {
// testnet
- expect(fixPubKeyChecksum("tpubVpeRVBDM14ydoJ7jCzTayDHiP6CU4NcbKEW6bWQGSghmi14BfNx3omc6GjV3AxbtkTYcPAedw48XywVgSyY8H9ef73zWWtoZ6MCMLLJiUyq")).toStrictEqual("tpubVpeRVBDM14ydoJ7jCzTayDHiP6CU4NcbKEW6bWQGSghmi14BfNx3omc6GjV3AxbtkTYcPAedw48XywVgSyY8H9ef73zWWtoZ6MCMLGM6GV3");
+ expect(fixPubKeyChecksum("tpubVpeRVBDM14ydoJ7jCzTayDHiP6CU4NcbKEW6bWQGSghmi14BfNx3omc6GjV3AxbtkTYcPAedw48XywVgSyY8H9ef73zWWtoZ6MCMLLJiUyq", true)).toStrictEqual("tpubVpeRVBDM14ydoJ7jCzTayDHiP6CU4NcbKEW6bWQGSghmi14BfNx3omc6GjV3AxbtkTYcPAedw48XywVgSyY8H9ef73zWWtoZ6MCMLGM6GV3");
// mainet
- expect(fixPubKeyChecksum("dpubZFs9f5ex4qxoiqCHVEnotAaYpZhCgwDAndMyLicfcXJysBmNxMyx7X3QHW2NiUN14KnekWcMQt4XwyF5wAqDPzbLcv59mQTRhL6foyvvqLK")).toStrictEqual("dpubZFs9f5ex4qxoiqCHVEnotAaYpZhCgwDAndMyLicfcXJysBmNxMyx7X3QHW2NiUN14KnekWcMQt4XwyF5wAqDPzbLcv59mQTRhL6fp1csmnS");
+ expect(fixPubKeyChecksum("dpubZFs9f5ex4qxoiqCHVEnotAaYpZhCgwDAndMyLicfcXJysBmNxMyx7X3QHW2NiUN14KnekWcMQt4XwyF5wAqDPzbLcv59mQTRhL6foyvvqLK", false)).toStrictEqual("dpubZFs9f5ex4qxoiqCHVEnotAaYpZhCgwDAndMyLicfcXJysBmNxMyx7X3QHW2NiUN14KnekWcMQt4XwyF5wAqDPzbLcv59mQTRhL6fp1csmnS");
});
function dispatchFn (inputs) {
diff --git a/yarn.lock b/yarn.lock
index d6edb6d758..dfd57ecc84 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1795,17 +1795,17 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
-"@ledgerhq/devices@^8.0.4":
- version "8.0.4"
- resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.0.4.tgz#ebc7779adbbec2d046424603a481623eb3fbe306"
- integrity sha512-dxOiWZmtEv1tgw70+rW8gviCRZUeGDUnxY6HUPiRqTAc0Ts2AXxiJChgAsPvIywWTGW+S67Nxq1oTZdpRbdt+A==
+"@ledgerhq/devices@^6.27.1":
+ version "6.27.1"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-6.27.1.tgz#3b13ab1d1ba8201e9e74a08f390560483978c962"
+ integrity sha512-jX++oy89jtv7Dp2X6gwt3MMkoajel80JFWcdc0HCouwDsV1mVJ3SQdwl/bQU0zd8HI6KebvUP95QTwbQLLK/RQ==
dependencies:
- "@ledgerhq/errors" "^6.12.7"
- "@ledgerhq/logs" "^6.10.1"
+ "@ledgerhq/errors" "^6.10.0"
+ "@ledgerhq/logs" "^6.10.0"
rxjs "6"
semver "^7.3.5"
-"@ledgerhq/errors@^6.12.7":
+"@ledgerhq/errors@^6.10.0":
version "6.12.7"
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.12.7.tgz#c7b630488d5713bc7b1e1682d6ab5d08918c69f1"
integrity sha512-1BpjzFErPK7qPFx0oItcX0mNLJMplVAm2Dpl5urZlubewnTyyw5sahIBjU+8LLCWJ2eGEh/0wyvh0jMtR0n2Mg==
@@ -1828,26 +1828,26 @@
tiny-secp256k1 "1.1.6"
varuint-bitcoin "1.1.2"
-"@ledgerhq/hw-transport-webusb@^6.27.16":
- version "6.27.16"
- resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.27.16.tgz#b8e20e772f78c312fc7f2ce3a469c99ecf59dc67"
- integrity sha512-A3S2p5Rh9Ot402pWNZw8v5EpO3wOHP8ch/Dcz0AjInmwNouQ9nIYd1+eLSL7QiyG9X7+tuHxFF1IjrEgvAzQuQ==
+"@ledgerhq/hw-transport-webusb@6.27.1":
+ version "6.27.1"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.27.1.tgz#62be3b2e5f0d54ae06d066571e4408a807d0f01f"
+ integrity sha512-n0ygJSeRpznrUfwtbDCLQOM5mA23YT/ngYY8HU46dzsVJHrHQ4jwBNJU48iKB+a9GhHyPAUpPNlWGTogvoVUxg==
dependencies:
- "@ledgerhq/devices" "^8.0.4"
- "@ledgerhq/errors" "^6.12.7"
- "@ledgerhq/hw-transport" "^6.28.5"
- "@ledgerhq/logs" "^6.10.1"
+ "@ledgerhq/devices" "^6.27.1"
+ "@ledgerhq/errors" "^6.10.0"
+ "@ledgerhq/hw-transport" "^6.27.1"
+ "@ledgerhq/logs" "^6.10.0"
-"@ledgerhq/hw-transport@^6.28.5":
- version "6.28.5"
- resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.28.5.tgz#675193be2f695a596068145351da598316c25831"
- integrity sha512-xmw5RhYbqExBBqTvOnOjN/RYNIGMBxFJ+zcYNfkfw/E+uEY3L7xq8Z7sC/n7URTT6xtEctElqduBJnBQE4OQtw==
+"@ledgerhq/hw-transport@6.27.1", "@ledgerhq/hw-transport@^6.27.1", "@ledgerhq/hw-transport@^6.28.5":
+ version "6.27.1"
+ resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.27.1.tgz#88072278f69c279cb6569352acd4ae2fec33ace3"
+ integrity sha512-hnE4/Fq1YzQI4PA1W0H8tCkI99R3UWDb3pJeZd6/Xs4Qw/q1uiQO+vNLC6KIPPhK0IajUfuI/P2jk0qWcMsuAQ==
dependencies:
- "@ledgerhq/devices" "^8.0.4"
- "@ledgerhq/errors" "^6.12.7"
+ "@ledgerhq/devices" "^6.27.1"
+ "@ledgerhq/errors" "^6.10.0"
events "^3.3.0"
-"@ledgerhq/logs@^6.10.1":
+"@ledgerhq/logs@^6.10.0", "@ledgerhq/logs@^6.10.1":
version "6.10.1"
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.1.tgz#5bd16082261d7364eabb511c788f00937dac588d"
integrity sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w==
@@ -10405,9 +10405,9 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
-"pi-ui@https://github.com/decred/pi-ui/":
+"pi-ui@https://github.com/decred/pi-ui#4daf214b901aa9f50547cf5645594cb76e7c9e51":
version "1.1.0"
- resolved "https://github.com/decred/pi-ui/#d7ca43e8bc65a45bbebd93b0101e58f3a6a170ab"
+ resolved "https://github.com/decred/pi-ui#4daf214b901aa9f50547cf5645594cb76e7c9e51"
dependencies:
clamp-js-main "^0.11.5"
lodash "^4.17.15"