Skip to content

Commit

Permalink
feat: support shielded tx with ledger (#1562)
Browse files Browse the repository at this point in the history
* feat: support ledger masp tx wip

* feat: unshielding working

* chore: cleanup

* feat: ledger masp with disposable signer

* feat: disposable signer for shielded transfers using ledger

* feat: proper flow for ledger txs from masp source
  • Loading branch information
mateuszjasiuk committed Feb 10, 2025
1 parent 3c580a1 commit d95dcbe
Show file tree
Hide file tree
Showing 20 changed files with 707 additions and 158 deletions.
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

0 comments on commit d95dcbe

Please sign in to comment.