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

feat: support shielded tx with ledger #1562

Merged
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
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
25 changes: 25 additions & 0 deletions apps/extension/src/background/approvals/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
QueryTxDetailsMsg,
RejectSignArbitraryMsg,
RejectSignTxMsg,
ReplaceMaspSignaturesMsg,
RevokeConnectionMsg,
SignMaspMsg,
SubmitApprovedSignArbitraryMsg,
SubmitApprovedSignLedgerTxMsg,
SubmitApprovedSignTxMsg,
Expand Down Expand Up @@ -113,6 +115,13 @@ export const getHandler: (service: ApprovalsService) => Handler = (service) => {
env,
msg as SubmitApprovedSignLedgerTxMsg
);
case ReplaceMaspSignaturesMsg:
return handleReplaceMaspSignaturesMsg(service)(
env,
msg as ReplaceMaspSignaturesMsg
);
case SignMaspMsg:
return handleSignMaspMsg(service)(env, msg as SignMaspMsg);

default:
throw new Error("Unknown msg type");
Expand Down Expand Up @@ -279,6 +288,22 @@ const handleSubmitApprovedSignLedgerTxMsg: (
};
};

const handleReplaceMaspSignaturesMsg: (
service: ApprovalsService
) => InternalHandler<ReplaceMaspSignaturesMsg> = (service) => {
return async (_, { msgId, signatures }) => {
return await service.replaceMaspSignatures(msgId, signatures);
};
};

const handleSignMaspMsg: (
service: ApprovalsService
) => InternalHandler<SignMaspMsg> = (service) => {
return async (_, { msgId, signer }) => {
return await service.signMasp(msgId, signer);
};
};

const handleCheckIsApprovedSite: (
service: ApprovalsService
) => InternalHandler<CheckIsApprovedSiteMsg> = (service) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/extension/src/background/approvals/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
QueryTxDetailsMsg,
RejectSignArbitraryMsg,
RejectSignTxMsg,
ReplaceMaspSignaturesMsg,
RevokeConnectionMsg,
SignMaspMsg,
SubmitApprovedSignArbitraryMsg,
SubmitApprovedSignLedgerTxMsg,
SubmitApprovedSignTxMsg,
Expand All @@ -36,6 +38,7 @@ export function init(router: Router, service: ApprovalsService): void {
router.registerMessage(SubmitApprovedSignTxMsg);
router.registerMessage(SubmitApprovedSignArbitraryMsg);
router.registerMessage(SubmitApprovedSignLedgerTxMsg);
router.registerMessage(ReplaceMaspSignaturesMsg);
router.registerMessage(IsConnectionApprovedMsg);
router.registerMessage(ApproveConnectInterfaceMsg);
router.registerMessage(ConnectInterfaceResponseMsg);
Expand All @@ -47,6 +50,7 @@ export function init(router: Router, service: ApprovalsService): void {
router.registerMessage(QueryTxDetailsMsg);
router.registerMessage(QuerySignArbitraryDataMsg);
router.registerMessage(QueryPendingTxBytesMsg);
router.registerMessage(SignMaspMsg);

router.addHandler(ROUTE, getHandler(service));
}
53 changes: 53 additions & 0 deletions apps/extension/src/background/approvals/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum MessageType {
SubmitApprovedSignTx = "submit-approved-sign-tx",
SubmitApprovedSignArbitrary = "submit-approved-sign-arbitrary",
SubmitApprovedSignLedgerTx = "submit-approved-sign-ledger-tx",
ReplaceMaspSignatures = "replace-masp-signatures",
RejectSignArbitrary = "reject-sign-arbitrary",
ConnectInterfaceResponse = "connect-interface-response",
DisconnectInterfaceResponse = "disconnect-interface-response",
Expand All @@ -19,6 +20,7 @@ export enum MessageType {
QuerySignArbitraryData = "query-sign-arbitrary-data",
QueryPendingTxBytes = "query-pending-tx-bytes",
CheckIsApprovedSite = "check-is-approved-site",
SignMaspMsg = "sign-masp",
}

export class SubmitApprovedSignTxMsg extends Message<void> {
Expand Down Expand Up @@ -46,6 +48,31 @@ export class SubmitApprovedSignTxMsg extends Message<void> {
}
}

export class SignMaspMsg extends Message<void> {
public static type(): MessageType {
return MessageType.SignMaspMsg;
}

constructor(
public readonly msgId: string,
public readonly signer: string
) {
super();
}

validate(): void {
validateProps(this, ["msgId", "signer"]);
}

route(): string {
return ROUTE;
}

type(): string {
return SignMaspMsg.type();
}
}

export class SubmitApprovedSignLedgerTxMsg extends Message<void> {
public static type(): MessageType {
return MessageType.SubmitApprovedSignLedgerTx;
Expand All @@ -71,6 +98,32 @@ export class SubmitApprovedSignLedgerTxMsg extends Message<void> {
}
}

export class ReplaceMaspSignaturesMsg extends Message<void> {
public static type(): MessageType {
return MessageType.ReplaceMaspSignatures;
}

constructor(
public readonly msgId: string,
// base64 encoded
public readonly signatures: string[]
) {
super();
}

validate(): void {
validateProps(this, ["msgId", "signatures"]);
}

route(): string {
return ROUTE;
}

type(): string {
return ReplaceMaspSignaturesMsg.type();
}
}

export class RejectSignTxMsg extends Message<void> {
public static type(): MessageType {
return MessageType.RejectSignTx;
Expand Down
Loading
Loading