Skip to content

Commit

Permalink
feat/746 - Updates for Ledger HW (shielded keys) (#1471)
Browse files Browse the repository at this point in the history
* feat: bump to latest Ledger package

* feat: begin hooking up shielded keys import

* feat: split proof-gen and viewing key calls, fix paths

* fix: fix tests

* feat: stealing Mateusz code, show payment address in confirmation

* feat: validate semver of installed app for Zip32

* feat: begin implementing import approval steps
  • Loading branch information
jurevans authored and mateuszjasiuk committed Feb 10, 2025
1 parent 035c2f6 commit e5040c2
Show file tree
Hide file tree
Showing 23 changed files with 388 additions and 165 deletions.
3 changes: 1 addition & 2 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@
"@cosmjs/encoding": "^0.29.0",
"@dao-xyz/borsh": "^5.1.5",
"@ledgerhq/hw-transport": "^6.31.4",
"@ledgerhq/hw-transport-webhid": "^6.29.4",
"@ledgerhq/hw-transport-webusb": "^6.29.4",
"@zondax/ledger-namada": "^1.0.0",
"@zondax/ledger-namada": "^2.0.0",
"bignumber.js": "^9.1.1",
"buffer": "^6.0.3",
"fp-ts": "^2.16.1",
Expand Down
10 changes: 4 additions & 6 deletions apps/extension/src/App/Accounts/ParentAccounts.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import invariant from "invariant";
import { useContext, useEffect } from "react";
import { Outlet, useNavigate } from "react-router-dom";

Expand Down Expand Up @@ -29,17 +30,14 @@ export const ParentAccounts = (): JSX.Element => {

// We check which accounts need to be re-imported
const accounts = allAccounts
.filter(
(account) => account.parentId || account.type === AccountType.Ledger
)
.filter((account) => account.parentId)
.map((account) => {
const outdated =
account.type !== AccountType.Ledger &&
typeof account.pseudoExtendedKey === "undefined";

// The only account without a parent is the ledger account
const parent =
parentAccounts.find((pa) => pa.id === account.parentId) || account;
const parent = parentAccounts.find((pa) => pa.id === account.parentId);
invariant(parent, `Parent account not found for account ${account.id}`);

return { ...parent, outdated };
});
Expand Down
5 changes: 0 additions & 5 deletions apps/extension/src/App/Accounts/UpdateRequired.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@ export const UpdateRequired = (): JSX.Element => {
</li>
</ol>
</Stack>
<p className="text-yellow text-center leading-3">
* Ledger accounts will receive shielded
<br /> functions in a separate update in an
<br /> upcoming release
</p>
</Stack>
</div>
</div>
Expand Down
8 changes: 4 additions & 4 deletions apps/extension/src/Setup/Common/Completion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import browser from "webextension-polyfill";
import { chains } from "@namada/chains";
import { ActionButton, Alert, Loading, ViewKeys } from "@namada/components";
import { makeBip44Path } from "@namada/sdk/web";
import { Bip44Path, DerivedAccount } from "@namada/types";
import { Bip44Path } from "@namada/types";
import {
AccountSecret,
AccountStore,
Expand All @@ -20,7 +20,7 @@ type Props = {
status?: CompletionStatus;
statusInfo: string;
parentAccountStore?: AccountStore;
shieldedAccount?: DerivedAccount;
paymentAddress?: string;
password?: string;
passwordRequired: boolean | undefined;
path: Bip44Path;
Expand All @@ -34,7 +34,7 @@ export const Completion: React.FC<Props> = (props) => {
passwordRequired,
path,
parentAccountStore,
shieldedAccount,
paymentAddress,
status,
statusInfo,
} = props;
Expand Down Expand Up @@ -84,7 +84,7 @@ export const Completion: React.FC<Props> = (props) => {
publicKeyAddress={parentAccountStore?.publicKey}
transparentAccountAddress={parentAccountStore?.address}
transparentAccountPath={transparentAccountPath}
shieldedAccountAddress={shieldedAccount?.address}
shieldedAccountAddress={paymentAddress}
trimCharacters={35}
footer={
<ActionButton
Expand Down
45 changes: 45 additions & 0 deletions apps/extension/src/Setup/Common/LedgerApprovalStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Heading, ProgressIndicator, Stack } from "@namada/components";

type LedgerApprovalStepProps = {
currentApprovalStep: number;
};

export const LedgerApprovalStep = ({
currentApprovalStep,
}: LedgerApprovalStepProps): JSX.Element => {
const stepText = [
"Deriving Bip44 public key...",
"Deriving Zip32 Viewing Key... This could take a few seconds!",
"Deriving Zip32 Proof-Generation Key... This could take a few seconds!",
];

// Ensure that steps are within stepText limits
const totalSteps = stepText.length;
const currentStep = Math.min(Math.max(currentApprovalStep, 1), totalSteps);

return (
<Stack gap={1} className="bg-black w-full p-4 rounded-md min-h-[240px]">
<Stack direction="horizontal" className="flex">
<span className="flex-none">
<ProgressIndicator
keyName="ledger-import"
totalSteps={totalSteps}
currentStep={currentStep}
/>
</span>
<span className="flex-1 text-white font-medium text-right">
Approval {currentStep}/{totalSteps}
</span>
</Stack>
<Heading
level="h2"
className="text-base text-center text-white font-medium"
>
Please wait for Ledger to respond!
</Heading>
<p className="font-medium text-yellow text-base text-center px-12">
{stepText[currentStep - 1]}
</p>
</Stack>
);
};
5 changes: 4 additions & 1 deletion apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export const LedgerConfirmation = (): JSX.Element => {
return <></>;
}

const account = location.state.account as DerivedAccount;
const account = location.state.account as DerivedAccount & {
paymentAddress: string;
};
return (
<Stack gap={4} className="h-[470px]">
<p className="text-white text-center text-base w-full -mt-3 mb-8">
Expand All @@ -22,6 +24,7 @@ export const LedgerConfirmation = (): JSX.Element => {
<ViewKeys
publicKeyAddress={account.publicKey}
transparentAccountAddress={account.address}
shieldedAccountAddress={account.paymentAddress}
trimCharacters={35}
/>
<ActionButton size="lg" onClick={closeCurrentTab}>
Expand Down
114 changes: 80 additions & 34 deletions apps/extension/src/Setup/Ledger/LedgerConnect.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { chains } from "@namada/chains";
import { ActionButton, Alert, Image, Stack } from "@namada/components";
import { Ledger as LedgerApp, makeBip44Path } from "@namada/sdk/web";
import {
ExtendedViewingKey,
Ledger as LedgerApp,
makeBip44Path,
makeSaplingPath,
ProofGenerationKey,
PseudoExtendedKey,
} from "@namada/sdk/web";
import initWasm from "@namada/sdk/web-init";
import { Bip44Path } from "@namada/types";
import { LedgerError } from "@zondax/ledger-namada";
import { LedgerStep } from "Setup/Common";
import { AdvancedOptions } from "Setup/Common/AdvancedOptions";
import Bip44Form from "Setup/Common/Bip44Form";
import { LedgerApprovalStep } from "Setup/Common/LedgerApprovalStep";
import routes from "Setup/routes";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
Expand All @@ -21,6 +30,9 @@ export const LedgerConnect: React.FC<Props> = ({ path, setPath }) => {
const [isLedgerConnecting, setIsLedgerConnecting] = useState(false);
const [ledger, setLedger] = useState<LedgerApp>();

// Import keys steps (transparent, viewing key, proof-gen key)
const [currentApprovalStep, setCurrentApprovalStep] = useState(1);

const queryLedger = async (ledger: LedgerApp): Promise<void> => {
setError(undefined);
try {
Expand All @@ -33,14 +45,47 @@ export const LedgerConnect: React.FC<Props> = ({ path, setPath }) => {
}

setIsLedgerConnecting(true);
setCurrentApprovalStep(1);
const { address, publicKey } = await ledger.showAddressAndPublicKey(
makeBip44Path(chains.namada.bip44.coinType, path)
);

// Shielded Keys
const zip32Path = makeSaplingPath(chains.namada.bip44.coinType, {
account: path.account,
});

setCurrentApprovalStep(2);
const { xfvk } = await ledger.getViewingKey(zip32Path);

setCurrentApprovalStep(3);
const { ak, nsk } = await ledger.getProofGenerationKey(zip32Path);

// SDK wasm init must be called
await initWasm();

const extendedViewingKey = new ExtendedViewingKey(xfvk);
const encodedExtendedViewingKey = extendedViewingKey.encode();
const encodedPaymentAddress = extendedViewingKey
.default_payment_address()
.encode();

const proofGenerationKey = ProofGenerationKey.from_bytes(ak, nsk);
const pseudoExtendedKey = PseudoExtendedKey.from(
extendedViewingKey,
proofGenerationKey
);
const encodedPseudoExtendedKey = pseudoExtendedKey.encode();

setIsLedgerConnecting(false);

navigate(routes.ledgerImport(), {
state: {
address,
publicKey,
extendedViewingKey: encodedExtendedViewingKey,
paymentAddress: encodedPaymentAddress,
pseudoExtendedKey: encodedPseudoExtendedKey,
},
});
} catch (e) {
Expand Down Expand Up @@ -83,48 +128,49 @@ export const LedgerConnect: React.FC<Props> = ({ path, setPath }) => {

return (
<Stack gap={6} className="justify-between min-h-[470px]">
<Stack
as="ol"
gap={4}
className="flex-1 justify-center mx-auto max-w-[400px]"
>
<Stack as="ol" gap={4} className="flex-1 justify-center mx-auto w-full">
{error && (
<Alert title="Error" type="error">
{error}
</Alert>
)}

{isLedgerConnecting && (
<Alert type="warning">Review on your Ledger</Alert>
<LedgerApprovalStep currentApprovalStep={currentApprovalStep} />
)}

<AdvancedOptions>
<Bip44Form path={path} setPath={setPath} />
</AdvancedOptions>

<LedgerStep
title="Step 1"
text="Connect and unlock your ledger Hardware Wallet"
onClick={() => connectUSB()}
active={!ledger}
complete={!!ledger}
buttonDisabled={!!ledger}
image={
<Image styleOverrides={{ width: "100%" }} imageName="Ledger" />
}
/>

<LedgerStep
title="Step 2"
text="Open the Namada App on your ledger device"
active={!!ledger}
complete={false}
onClick={() => connectNamadaApp()}
buttonDisabled={!ledger || isLedgerConnecting}
image={
<Image styleOverrides={{ width: "100%" }} imageName="LogoMinimal" />
}
/>
{!isLedgerConnecting && (
<>
<AdvancedOptions>
<Bip44Form path={path} setPath={setPath} />
</AdvancedOptions>
<LedgerStep
title="Step 1"
text="Connect and unlock your ledger Hardware Wallet"
onClick={() => connectUSB()}
active={!ledger}
complete={!!ledger}
buttonDisabled={!!ledger}
image={
<Image styleOverrides={{ width: "100%" }} imageName="Ledger" />
}
/>
<LedgerStep
title="Step 2"
text="Open the Namada App on your ledger device"
active={!!ledger}
complete={false}
onClick={() => connectNamadaApp()}
buttonDisabled={!ledger || isLedgerConnecting}
image={
<Image
styleOverrides={{ width: "100%" }}
imageName="LogoMinimal"
/>
}
/>
</>
)}
</Stack>
<ActionButton size="lg" disabled={true}>
Next
Expand Down
16 changes: 14 additions & 2 deletions apps/extension/src/Setup/Ledger/LedgerImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { useLocation, useNavigate } from "react-router-dom";
type LedgerImportLocationState = {
address: string;
publicKey: string;
extendedViewingKey: string;
pseudoExtendedKey: string;
paymentAddress: string;
};

type LedgerProps = {
Expand Down Expand Up @@ -55,16 +58,25 @@ export const LedgerImport = ({
await accountManager.savePassword(password);
}

const { address, publicKey } = locationState;
const {
address,
publicKey,
extendedViewingKey,
paymentAddress,
pseudoExtendedKey,
} = locationState;
const account = await accountManager.saveLedgerAccount({
alias,
address,
publicKey,
path,
paymentAddress,
extendedViewingKey,
pseudoExtendedKey,
});

navigate(routes.ledgerComplete(), {
state: { account: { ...account } },
state: { account: { ...account, paymentAddress } },
});
} catch (e) {
console.warn(e);
Expand Down
Loading

0 comments on commit e5040c2

Please sign in to comment.