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 #1461

Closed
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
52 changes: 48 additions & 4 deletions apps/extension/src/Approvals/ConfirmSignLedgerTx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ 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 } 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 { PageHeader } from "App/Common";
import { ApprovalDetails, Status } from "Approvals/Approvals";
import {
QueryPendingTxBytesMsg,
ReplaceMaspSignatureMsg,
SubmitApprovedSignLedgerTxMsg,
} from "background/approvals";
import { QueryAccountDetailsMsg } from "background/keyring";
Expand Down Expand Up @@ -72,6 +73,32 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
}
}, [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 Down Expand Up @@ -135,6 +162,9 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
index: accountDetails.path.index || 0,
};
const bip44Path = makeBip44Path(chains.namada.bip44.coinType, path);
const zip32Path = makeSaplingPath(chains.namada.bip44.coinType, {
account: path.account,
});
const pendingTxs = await requester.sendMessage(
Ports.Background,
new QueryPendingTxBytesMsg(msgId)
Expand All @@ -147,6 +177,7 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
}

const signatures: ResponseSign[] = [];
const maspSignatures: string[] = [];

let txIndex = 0;
const txCount = pendingTxs.length;
Expand All @@ -166,9 +197,22 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
</p>
);
}
const signature = await signLedgerTx(
const { sbar, rbar } = await signMaspTx(
ledger,
fromBase64(tx),
zip32Path
);
const maspSignature = toBase64(new Uint8Array([...rbar, ...sbar]));
maspSignatures.push(maspSignature);

const txWithMaspSection = await requester.sendMessage(
Ports.Background,
new ReplaceMaspSignatureMsg(msgId, maspSignature, txIndex)
);

const signature = await signLedgerTx(
ledger,
fromBase64(txWithMaspSection),
bip44Path
);
signatures.push(signature);
Expand All @@ -178,7 +222,7 @@ export const ConfirmSignLedgerTx: React.FC<Props> = ({ details }) => {
setStepTwoDescription(<p>Submitting...</p>);
await requester.sendMessage(
Ports.Background,
new SubmitApprovedSignLedgerTxMsg(msgId, signatures)
new SubmitApprovedSignLedgerTxMsg(msgId, signatures, maspSignatures)
);

setStatus(Status.Completed);
Expand Down
26 changes: 24 additions & 2 deletions apps/extension/src/background/approvals/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
QueryTxDetailsMsg,
RejectSignArbitraryMsg,
RejectSignTxMsg,
ReplaceMaspSignatureMsg,
RevokeConnectionMsg,
SubmitApprovedSignArbitraryMsg,
SubmitApprovedSignLedgerTxMsg,
Expand Down Expand Up @@ -113,6 +114,11 @@ export const getHandler: (service: ApprovalsService) => Handler = (service) => {
env,
msg as SubmitApprovedSignLedgerTxMsg
);
case ReplaceMaspSignatureMsg:
return handleReplaceMaspSignatureMsg(service)(
env,
msg as ReplaceMaspSignatureMsg
);

default:
throw new Error("Unknown msg type");
Expand Down Expand Up @@ -274,8 +280,24 @@ const handleQuerySignArbitraryData: (
const handleSubmitApprovedSignLedgerTxMsg: (
service: ApprovalsService
) => InternalHandler<SubmitApprovedSignLedgerTxMsg> = (service) => {
return async ({ senderTabId: popupTabId }, { msgId, responseSign }) => {
return await service.submitSignLedgerTx(popupTabId, msgId, responseSign);
return async (
{ senderTabId: popupTabId },
{ msgId, responseSign, maspSignatures }
) => {
return await service.submitSignLedgerTx(
popupTabId,
msgId,
responseSign,
maspSignatures
);
};
};

const handleReplaceMaspSignatureMsg: (
service: ApprovalsService
) => InternalHandler<ReplaceMaspSignatureMsg> = (service) => {
return async (_, { msgId, signature, txIndex }) => {
return await service.replaceMaspSignature(msgId, signature, txIndex);
};
};

Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/background/approvals/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
QueryTxDetailsMsg,
RejectSignArbitraryMsg,
RejectSignTxMsg,
ReplaceMaspSignatureMsg,
RevokeConnectionMsg,
SubmitApprovedSignArbitraryMsg,
SubmitApprovedSignLedgerTxMsg,
Expand All @@ -36,6 +37,7 @@ export function init(router: Router, service: ApprovalsService): void {
router.registerMessage(SubmitApprovedSignTxMsg);
router.registerMessage(SubmitApprovedSignArbitraryMsg);
router.registerMessage(SubmitApprovedSignLedgerTxMsg);
router.registerMessage(ReplaceMaspSignatureMsg);
router.registerMessage(IsConnectionApprovedMsg);
router.registerMessage(ApproveConnectInterfaceMsg);
router.registerMessage(ConnectInterfaceResponseMsg);
Expand Down
36 changes: 34 additions & 2 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",
ReplaceMaspSignature = "replace-masp-signature",
RejectSignArbitrary = "reject-sign-arbitrary",
ConnectInterfaceResponse = "connect-interface-response",
DisconnectInterfaceResponse = "disconnect-interface-response",
Expand Down Expand Up @@ -53,13 +54,15 @@ export class SubmitApprovedSignLedgerTxMsg extends Message<void> {

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

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

route(): string {
Expand All @@ -71,6 +74,35 @@ export class SubmitApprovedSignLedgerTxMsg extends Message<void> {
}
}

// returns base64 encoded tx
export class ReplaceMaspSignatureMsg extends Message<string> {
public static type(): MessageType {
return MessageType.ReplaceMaspSignature;
}

constructor(
public readonly msgId: string,
// base64 encoded
public readonly signature: string,
// pending tx index
public readonly txIndex: number
) {
super();
}

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

route(): string {
return ROUTE;
}

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

export class RejectSignTxMsg extends Message<void> {
public static type(): MessageType {
return MessageType.RejectSignTx;
Expand Down
54 changes: 49 additions & 5 deletions apps/extension/src/background/approvals/service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { toBase64 } from "@cosmjs/encoding";
import { fromBase64, toBase64 } from "@cosmjs/encoding";
import { v4 as uuid } from "uuid";
import browser, { Windows } from "webextension-polyfill";

import { KVStore } from "@namada/storage";
import { SignArbitraryResponse, TxDetails } from "@namada/types";
import {
Message,
SignArbitraryResponse,
SigningDataMsgValue,
TxDetails,
} from "@namada/types";
import { paramsToUrl } from "@namada/utils";

import { ResponseSign } from "@zondax/ledger-namada";
Expand Down Expand Up @@ -131,7 +136,8 @@ export class ApprovalsService {
async submitSignLedgerTx(
popupTabId: number,
msgId: string,
responseSign: ResponseSign[]
responseSign: ResponseSign[],
maspSignatures: string[]
): Promise<void> {
const pendingTx = await this.txStore.get(msgId);
const resolvers = this.getResolver(popupTabId);
Expand All @@ -147,8 +153,21 @@ export class ApprovalsService {
const { tx } = this.sdkService.getSdk();

try {
const signedTxs = pendingTx.txs.map(({ bytes }, i) => {
return tx.appendSignature(bytes, responseSign[i]);
const signedTxs = pendingTx.txs.map((pendingTx, i) => {
const signingData = pendingTx.signingData.map((signingData) =>
new Message().encode(new SigningDataMsgValue(signingData))
);
const maspSignature = fromBase64(maspSignatures[i]);
// TODO: consider improving this, explanation below:
// Because we read tx from the store we have to append masp signatures twice
// First time before ledger computes tx wrapper signature and second time now to submit proper tx
const txWithMasp = tx.appendMaspSignature(
pendingTx.bytes,
signingData,
maspSignature
);

return tx.appendSignature(txWithMasp, responseSign[i]);
});
resolvers.resolve(signedTxs);
} catch (e) {
Expand All @@ -158,6 +177,31 @@ export class ApprovalsService {
await this.clearPendingSignature(msgId);
}

async replaceMaspSignature(
msgId: string,
signature: string,
txIndex: number
): Promise<string> {
const pendingTx = (await this.txStore.get(msgId))?.txs.at(txIndex);

if (!pendingTx) {
throw new Error(ApprovalErrors.TransactionDataNotFound(msgId));
}

const { tx } = this.sdkService.getSdk();

const signingData = pendingTx.signingData.map((signingData) =>
new Message().encode(new SigningDataMsgValue(signingData))
);
const txWithMasp = tx.appendMaspSignature(
pendingTx.bytes,
signingData,
fromBase64(signature)
);

return toBase64(txWithMasp);
}

async submitSignArbitrary(
popupTabId: number,
msgId: string,
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/background/keyring/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ const handleQueryAccountsMsg: (
const output =
query && query.accountId ?
await service.queryAccountsByParentId(query.accountId)
: await service.queryAccounts();
: await service.queryAccounts();

return output;
};
Expand Down
6 changes: 3 additions & 3 deletions apps/extension/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const plugins = [
MANIFEST_PATH,
...(NODE_ENV === "development" && TARGET === "firefox" ?
[MANIFEST_V2_DEV_ONLY_PATH]
: []),
: []),
],
output: {
fileName: "./manifest.json",
Expand Down Expand Up @@ -140,7 +140,7 @@ module.exports = {
devtool:
NODE_ENV === "development" && TARGET === "firefox" ?
"eval-source-map"
: false,
: false,
entry: {
content: "./src/content",
background: "./src/background",
Expand Down Expand Up @@ -224,7 +224,7 @@ module.exports = {
hints: "warning",
maxAssetSize: 200000,
maxEntrypointSize: 400000,
assetFilter: function (assetFilename) {
assetFilter: function(assetFilename) {
assetFilename.endsWith(".wasm");
},
},
Expand Down
Loading
Loading