Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Ledger MASP Integration #1575

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
17 changes: 6 additions & 11 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 All @@ -7,7 +8,7 @@ import {
KeyListItem,
Stack,
} from "@namada/components";
import { AccountType, DerivedAccount } from "@namada/types";
import { DerivedAccount } from "@namada/types";
import { ParentAccountsFooter } from "App/Accounts/ParentAccountsFooter";
import { PageHeader } from "App/Common";
import routes from "App/routes";
Expand All @@ -26,20 +27,14 @@ export const ParentAccounts = (): JSX.Element => {
accounts: allAccounts,
changeActiveAccountId,
} = useContext(AccountContext);

// 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";
const outdated = 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
136 changes: 119 additions & 17 deletions apps/extension/src/Approvals/ConfirmSignLedgerTx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@ import clsx from "clsx";
import { ReactNode, useCallback, useEffect, useState } from "react";

import { ActionButton, Stack } from "@namada/components";
import { Ledger, makeBip44Path } from "@namada/sdk/web";
import {
Ledger,
makeBip44Path,
makeSaplingPath,
TxType,
} from "@namada/sdk/web";
import { LedgerError, ResponseSign } from "@zondax/ledger-namada";

import { fromBase64 } from "@cosmjs/encoding";
import { fromBase64, toBase64 } from "@cosmjs/encoding";
import { chains } from "@namada/chains";
import { TransferProps } from "@namada/types";
import { PageHeader } from "App/Common";
import { ApprovalDetails, Status } from "Approvals/Approvals";
import {
QueryPendingTxBytesMsg,
ReplaceMaspSignaturesMsg,
SubmitApprovedSignLedgerTxMsg,
SubmitApprovedSignTxMsg,
} from "background/approvals";
import { QueryAccountDetailsMsg } from "background/keyring";
import { useRequester } from "hooks/useRequester";
import { Ports } from "router";
import { closeCurrentTab } from "utils";
import { closeCurrentTab, parseTransferType } from "utils";
import { ApproveIcon } from "./ApproveIcon";
import { LedgerIcon } from "./LedgerIcon";
import { StatusBox } from "./StatusBox";
Expand Down Expand Up @@ -64,14 +72,40 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
useState<React.ReactNode>();
const [isLedgerConnected, setIsLedgerConnected] = useState(false);
const [ledger, setLedger] = useState<Ledger>();
const { msgId, signer } = details;
const { msgId, signer, txDetails } = details;

useEffect(() => {
if (status === Status.Completed) {
void closeCurrentTab();
}
}, [status]);

const signMaspTx = async (
ledger: Ledger,
bytes: Uint8Array,
path: string
): Promise<{ sbar: Uint8Array; rbar: Uint8Array }> => {
const signMaspSpendsResponse = await ledger.namadaApp.signMaspSpends(
path,
Buffer.from(bytes)
);

if (signMaspSpendsResponse.returnCode !== LedgerError.NoErrors) {
throw new Error(
`Signing masp spends error encountered: ${signMaspSpendsResponse.errorMessage}`
);
}

const spendSignatureResponse = await ledger.namadaApp.getSpendSignature();
if (spendSignatureResponse.returnCode !== LedgerError.NoErrors) {
throw new Error(
`Getting spends signature error encountered: ${signMaspSpendsResponse.errorMessage}`
);
}

return spendSignatureResponse;
};

const signLedgerTx = async (
ledger: Ledger,
bytes: Uint8Array,
Expand All @@ -90,6 +124,37 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
return signature;
};

const handleMaspSignTx = useCallback(
async (
ledger: Ledger,
tx: string,
zip32Path: string,
signatures: string[]
) => {
const { sbar, rbar } = await signMaspTx(
ledger,
fromBase64(tx),
zip32Path
);
const signature = toBase64(new Uint8Array([...rbar, ...sbar]));
signatures.push(signature);
},
[]
);

const handleSignTx = useCallback(
async (
ledger: Ledger,
tx: string,
bip44Path: string,
signatures: ResponseSign[]
) => {
const signature = await signLedgerTx(ledger, fromBase64(tx), bip44Path);
signatures.push(signature);
},
[]
);

const handleApproveLedgerSignTx = useCallback(
async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
Expand Down Expand Up @@ -122,6 +187,8 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
setStepTwoDescription("Preparing transaction...");

try {
// TODO: we have to check if the signer is disposable or not

const accountDetails = await requester.sendMessage(
Ports.Background,
new QueryAccountDetailsMsg(signer)
Expand All @@ -134,7 +201,7 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
change: accountDetails.path.change || 0,
index: accountDetails.path.index || 0,
};
const bip44Path = makeBip44Path(chains.namada.bip44.coinType, path);

const pendingTxs = await requester.sendMessage(
Ports.Background,
new QueryPendingTxBytesMsg(msgId)
Expand All @@ -146,8 +213,6 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
);
}

const signatures: ResponseSign[] = [];

let txIndex = 0;
const txCount = pendingTxs.length;
const stepTwoText = "Approve on your device";
Expand All @@ -156,6 +221,24 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
setStepTwoDescription(<p>{stepTwoText}</p>);
}

// Those collections are being mutated in the loop
const signatures: ResponseSign[] = [];
const maspSignatures: string[] = [];

const transferTypes = txDetails.flatMap((details) =>
details.commitments
.filter((cmt) => cmt.txType === TxType.Transfer)
.map(
(cmt) =>
parseTransferType(cmt as TransferProps, details.wrapperFeePayer)
.type
)
);
// For now we work under the assumption that we can't batch transfers from masp with other tx types
const fromMasp =
transferTypes.includes("Shielded") ||
transferTypes.includes("Unshielding");

for await (const tx of pendingTxs) {
if (txCount > 1) {
setStepTwoDescription(
Expand All @@ -166,20 +249,39 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
</p>
);
}
const signature = await signLedgerTx(
ledger,
fromBase64(tx),
bip44Path
);
signatures.push(signature);

if (fromMasp) {
const zip32Path = makeSaplingPath(chains.namada.bip44.coinType, {
account: path.account,
});
// Adds new signature to the collection
await handleMaspSignTx(ledger, tx, zip32Path, maspSignatures);
} else {
const bip44Path = makeBip44Path(chains.namada.bip44.coinType, path);
// Adds new signature to the collection
await handleSignTx(ledger, tx, bip44Path, signatures);
}

txIndex++;
}

setStepTwoDescription(<p>Submitting...</p>);
await requester.sendMessage(
Ports.Background,
new SubmitApprovedSignLedgerTxMsg(msgId, signatures)
);

if (fromMasp) {
await requester.sendMessage(
Ports.Background,
new ReplaceMaspSignaturesMsg(msgId, maspSignatures)
);
await requester.sendMessage(
Ports.Background,
new SubmitApprovedSignTxMsg(msgId, signer)
);
} else {
await requester.sendMessage(
Ports.Background,
new SubmitApprovedSignLedgerTxMsg(msgId, signatures)
);
}

setStatus(Status.Completed);
} catch (e) {
Expand Down
9 changes: 8 additions & 1 deletion apps/extension/src/Approvals/ConfirmSignTx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
import { ActionButton, Input, Stack } from "@namada/components";
import { PageHeader } from "App/Common";
import { ApprovalDetails, Status } from "Approvals/Approvals";
import { SubmitApprovedSignTxMsg } from "background/approvals";
import { SignMaspMsg, SubmitApprovedSignTxMsg } from "background/approvals";
import { UnlockVaultMsg } from "background/vault";
import { useRequester } from "hooks/useRequester";
import { Ports } from "router";
Expand Down Expand Up @@ -41,6 +41,13 @@ export const ConfirmSignTx: React.FC<Props> = ({ details }) => {
throw new Error("Invalid password!");
}

// TODO: ideally we should only calling this for Unshielding and Shielded Transfers,
// it should not break anything it's just unnecessary computation
await requester.sendMessage(
Ports.Background,
new SignMaspMsg(msgId, signer)
);

await requester.sendMessage(
Ports.Background,
new SubmitApprovedSignTxMsg(msgId, signer)
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>
);
};
Loading