@@ -22,6 +24,7 @@ export const LedgerConfirmation = (): JSX.Element => {
diff --git a/apps/extension/src/Setup/Ledger/LedgerConnect.tsx b/apps/extension/src/Setup/Ledger/LedgerConnect.tsx
index 8dd0d176a4..7a09c719b8 100644
--- a/apps/extension/src/Setup/Ledger/LedgerConnect.tsx
+++ b/apps/extension/src/Setup/Ledger/LedgerConnect.tsx
@@ -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";
@@ -21,6 +30,9 @@ export const LedgerConnect: React.FC = ({ path, setPath }) => {
const [isLedgerConnecting, setIsLedgerConnecting] = useState(false);
const [ledger, setLedger] = useState();
+ // Import keys steps (transparent, viewing key, proof-gen key)
+ const [currentApprovalStep, setCurrentApprovalStep] = useState(1);
+
const queryLedger = async (ledger: LedgerApp): Promise => {
setError(undefined);
try {
@@ -33,14 +45,47 @@ export const LedgerConnect: React.FC = ({ 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) {
@@ -83,11 +128,7 @@ export const LedgerConnect: React.FC = ({ path, setPath }) => {
return (
-
+
{error && (
{error}
@@ -95,36 +136,41 @@ export const LedgerConnect: React.FC = ({ path, setPath }) => {
)}
{isLedgerConnecting && (
- Review on your Ledger
+
)}
-
-
-
-
- connectUSB()}
- active={!ledger}
- complete={!!ledger}
- buttonDisabled={!!ledger}
- image={
-
- }
- />
-
- connectNamadaApp()}
- buttonDisabled={!ledger || isLedgerConnecting}
- image={
-
- }
- />
+ {!isLedgerConnecting && (
+ <>
+
+
+
+ connectUSB()}
+ active={!ledger}
+ complete={!!ledger}
+ buttonDisabled={!!ledger}
+ image={
+
+ }
+ />
+ connectNamadaApp()}
+ buttonDisabled={!ledger || isLedgerConnecting}
+ image={
+
+ }
+ />
+ >
+ )}
Next
diff --git a/apps/extension/src/Setup/Ledger/LedgerImport.tsx b/apps/extension/src/Setup/Ledger/LedgerImport.tsx
index 6d9fb4ef50..55b9c3598d 100644
--- a/apps/extension/src/Setup/Ledger/LedgerImport.tsx
+++ b/apps/extension/src/Setup/Ledger/LedgerImport.tsx
@@ -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 = {
@@ -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);
diff --git a/apps/extension/src/Setup/Setup.tsx b/apps/extension/src/Setup/Setup.tsx
index c08e1a3ea5..28ea9a07ba 100644
--- a/apps/extension/src/Setup/Setup.tsx
+++ b/apps/extension/src/Setup/Setup.tsx
@@ -11,7 +11,7 @@ import {
Container,
LifecycleExecutionWrapper as Wrapper,
} from "@namada/components";
-import { Bip44Path, DerivedAccount } from "@namada/types";
+import { Bip44Path } from "@namada/types";
import { assertNever } from "@namada/utils";
import { AccountSecret, AccountStore } from "background/keyring";
import { AnimatePresence, motion } from "framer-motion";
@@ -81,7 +81,7 @@ export const Setup: React.FC = () => {
});
const [parentAccountStore, setParentAccountStore] = useState();
- const [shieldedAccount, setShieldedAccount] = useState();
+ const [paymentAddress, setPaymentAddress] = useState();
const [completionStatus, setCompletionStatus] = useState();
const [completionStatusInfo, setCompletionStatusInfo] = useState("");
@@ -127,7 +127,7 @@ export const Setup: React.FC = () => {
details,
parentAccount
);
- setShieldedAccount(shieldedAccount);
+ setPaymentAddress(shieldedAccount?.address);
setCompletionStatus(CompletionStatus.Completed);
setCompletionStatusInfo("Done!");
} catch (e) {
@@ -275,7 +275,7 @@ export const Setup: React.FC = () => {
password={accountCreationDetails.password || ""}
path={path}
parentAccountStore={parentAccountStore}
- shieldedAccount={shieldedAccount}
+ paymentAddress={paymentAddress}
status={completionStatus}
statusInfo={completionStatusInfo}
/>
@@ -352,7 +352,7 @@ export const Setup: React.FC = () => {
password={accountCreationDetails.password || ""}
path={path}
parentAccountStore={parentAccountStore}
- shieldedAccount={shieldedAccount}
+ paymentAddress={paymentAddress}
status={completionStatus}
statusInfo={completionStatusInfo}
/>
@@ -398,7 +398,7 @@ export const Setup: React.FC = () => {
+
}
diff --git a/apps/extension/src/Setup/query.ts b/apps/extension/src/Setup/query.ts
index ea8be11553..f7afe1e89d 100644
--- a/apps/extension/src/Setup/query.ts
+++ b/apps/extension/src/Setup/query.ts
@@ -88,10 +88,26 @@ export class AccountManager {
async saveLedgerAccount(
details: LedgerAccountDetails
): Promise {
- const { alias, address, publicKey, path } = details;
+ const {
+ alias,
+ address,
+ publicKey,
+ path,
+ extendedViewingKey,
+ pseudoExtendedKey,
+ paymentAddress,
+ } = details;
return (await this.requester.sendMessage(
Ports.Background,
- new AddLedgerAccountMsg(alias, address, publicKey, path)
+ new AddLedgerAccountMsg(
+ alias,
+ address,
+ publicKey,
+ path,
+ extendedViewingKey,
+ pseudoExtendedKey,
+ paymentAddress
+ )
)) as AccountStore;
}
}
diff --git a/apps/extension/src/Setup/types.ts b/apps/extension/src/Setup/types.ts
index 4ea8253ed3..54d7901c91 100644
--- a/apps/extension/src/Setup/types.ts
+++ b/apps/extension/src/Setup/types.ts
@@ -19,4 +19,7 @@ export type LedgerAccountDetails = {
path: Bip44Path;
address: string;
publicKey: string;
+ extendedViewingKey: string;
+ pseudoExtendedKey: string;
+ paymentAddress: string;
};
diff --git a/apps/extension/src/background/approvals/handler.ts b/apps/extension/src/background/approvals/handler.ts
index 0f9c3665f9..b03e2c8dff 100644
--- a/apps/extension/src/background/approvals/handler.ts
+++ b/apps/extension/src/background/approvals/handler.ts
@@ -16,7 +16,9 @@ import {
QueryTxDetailsMsg,
RejectSignArbitraryMsg,
RejectSignTxMsg,
+ ReplaceMaspSignaturesMsg,
RevokeConnectionMsg,
+ SignMaspMsg,
SubmitApprovedSignArbitraryMsg,
SubmitApprovedSignLedgerTxMsg,
SubmitApprovedSignTxMsg,
@@ -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");
@@ -279,6 +288,22 @@ const handleSubmitApprovedSignLedgerTxMsg: (
};
};
+const handleReplaceMaspSignaturesMsg: (
+ service: ApprovalsService
+) => InternalHandler = (service) => {
+ return async (_, { msgId, signatures }) => {
+ return await service.replaceMaspSignatures(msgId, signatures);
+ };
+};
+
+const handleSignMaspMsg: (
+ service: ApprovalsService
+) => InternalHandler = (service) => {
+ return async (_, { msgId, signer }) => {
+ return await service.signMasp(msgId, signer);
+ };
+};
+
const handleCheckIsApprovedSite: (
service: ApprovalsService
) => InternalHandler = (service) => {
diff --git a/apps/extension/src/background/approvals/init.ts b/apps/extension/src/background/approvals/init.ts
index 37e95732a1..e7d534eaa3 100644
--- a/apps/extension/src/background/approvals/init.ts
+++ b/apps/extension/src/background/approvals/init.ts
@@ -16,7 +16,9 @@ import {
QueryTxDetailsMsg,
RejectSignArbitraryMsg,
RejectSignTxMsg,
+ ReplaceMaspSignaturesMsg,
RevokeConnectionMsg,
+ SignMaspMsg,
SubmitApprovedSignArbitraryMsg,
SubmitApprovedSignLedgerTxMsg,
SubmitApprovedSignTxMsg,
@@ -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);
@@ -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));
}
diff --git a/apps/extension/src/background/approvals/messages.ts b/apps/extension/src/background/approvals/messages.ts
index 400ecf2991..397c5978dc 100644
--- a/apps/extension/src/background/approvals/messages.ts
+++ b/apps/extension/src/background/approvals/messages.ts
@@ -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",
@@ -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 {
@@ -46,6 +48,31 @@ export class SubmitApprovedSignTxMsg extends Message {
}
}
+export class SignMaspMsg extends Message {
+ 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 {
public static type(): MessageType {
return MessageType.SubmitApprovedSignLedgerTx;
@@ -71,6 +98,32 @@ export class SubmitApprovedSignLedgerTxMsg extends Message {
}
}
+export class ReplaceMaspSignaturesMsg extends Message {
+ 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 {
public static type(): MessageType {
return MessageType.RejectSignTx;
diff --git a/apps/extension/src/background/approvals/service.ts b/apps/extension/src/background/approvals/service.ts
index 88964bef99..64e372f728 100644
--- a/apps/extension/src/background/approvals/service.ts
+++ b/apps/extension/src/background/approvals/service.ts
@@ -1,9 +1,15 @@
-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,
+ TxProps,
+} from "@namada/types";
import { paramsToUrl } from "@namada/utils";
import { ResponseSign } from "@zondax/ledger-namada";
@@ -147,8 +153,8 @@ 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) => {
+ return tx.appendSignature(pendingTx.bytes, responseSign[i]);
});
resolvers.resolve(signedTxs);
} catch (e) {
@@ -158,6 +164,71 @@ export class ApprovalsService {
await this.clearPendingSignature(msgId);
}
+ /**
+ * Modifies pending transaction data by appending real MASP signatures
+ *
+ * @async
+ * @param {string} msgId - message ID
+ * @param {string} signer - signer
+ * @throws {Error} - if pending transaction data is not found
+ * @returns void
+ */
+ async signMasp(msgId: string, signer: string): Promise {
+ const pendingTx = await this.txStore.get(msgId);
+
+ if (!pendingTx) {
+ throw new Error(ApprovalErrors.PendingSigningDataNotFound(msgId));
+ }
+
+ const txs: TxProps[] = [];
+ for await (const tx of pendingTx.txs) {
+ const bytes = await this.keyRingService.signMasp(tx, signer);
+ txs.push({
+ ...tx,
+ bytes,
+ });
+ }
+
+ await this.txStore.set(msgId, { ...pendingTx, txs });
+ }
+
+ /**
+ * Modifies pending transaction data by replacing MASP signatures
+ *
+ * @async
+ * @param {string} msgId - message ID
+ * @param {string[]} signatures - MASP signatures
+ * @throws {Error} - if pending transaction data is not found
+ * @returns void
+ */
+ async replaceMaspSignatures(
+ msgId: string,
+ signatures: string[]
+ ): Promise {
+ const pendingTx = await this.txStore.get(msgId);
+ if (!pendingTx) {
+ throw new Error(ApprovalErrors.TransactionDataNotFound(msgId));
+ }
+
+ const { tx: sdkTx } = this.sdkService.getSdk();
+
+ const txsWithSignatures = signatures.map((signature, i) => {
+ const tx = pendingTx.txs[i];
+ const signingData = tx.signingData.map((signingData) =>
+ new Message().encode(new SigningDataMsgValue(signingData))
+ );
+ const txBytes = sdkTx.appendMaspSignature(
+ tx.bytes,
+ signingData,
+ fromBase64(signature)
+ );
+
+ return { ...tx, bytes: txBytes };
+ });
+
+ await this.txStore.set(msgId, { ...pendingTx, txs: txsWithSignatures });
+ }
+
async submitSignArbitrary(
popupTabId: number,
msgId: string,
diff --git a/apps/extension/src/background/keyring/handler.ts b/apps/extension/src/background/keyring/handler.ts
index 2d86d20d21..e447d5f949 100644
--- a/apps/extension/src/background/keyring/handler.ts
+++ b/apps/extension/src/background/keyring/handler.ts
@@ -105,8 +105,24 @@ const handleAddLedgerAccountMsg: (
service: KeyRingService
) => InternalHandler = (service) => {
return async (_, msg) => {
- const { alias, address, publicKey, bip44Path } = msg;
- return await service.saveLedger(alias, address, publicKey, bip44Path);
+ const {
+ alias,
+ address,
+ publicKey,
+ bip44Path,
+ extendedViewingKey,
+ pseudoExtendedKey,
+ paymentAddress,
+ } = msg;
+ return await service.saveLedger(
+ alias,
+ address,
+ publicKey,
+ bip44Path,
+ extendedViewingKey,
+ pseudoExtendedKey,
+ paymentAddress
+ );
};
};
diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts
index 54352f3233..2ca1136a6a 100644
--- a/apps/extension/src/background/keyring/keyring.ts
+++ b/apps/extension/src/background/keyring/keyring.ts
@@ -101,7 +101,10 @@ export class KeyRing {
alias: string,
address: string,
publicKey: string,
- bip44Path: Bip44Path
+ bip44Path: Bip44Path,
+ pseudoExtendedKey: string,
+ extendedViewingKey: string,
+ paymentAddress: string
): Promise {
const id = generateId(UUID_NAMESPACE, alias, address);
const accountStore: AccountStore = {
@@ -125,6 +128,30 @@ export class KeyRing {
sensitive,
});
+ const shieldedId = generateId(UUID_NAMESPACE, alias, paymentAddress);
+ const shieldedAccountStore: AccountStore = {
+ id: shieldedId,
+ alias,
+ address: paymentAddress,
+ publicKey,
+ owner: extendedViewingKey,
+ path: bip44Path,
+ pseudoExtendedKey,
+ parentId: id,
+ type: AccountType.ShieldedKeys,
+ source: "imported",
+ timestamp: 0,
+ };
+
+ const shieldedSensitive = await this.vaultService.encryptSensitiveData({
+ text: "",
+ passphrase: "",
+ });
+ await this.vaultStorage.add(KeyStore, {
+ public: shieldedAccountStore,
+ sensitive: shieldedSensitive,
+ });
+
await this.setActiveAccount(id, AccountType.Ledger);
return accountStore;
}
@@ -712,7 +739,6 @@ export class KeyRing {
chainId: string
): Promise {
await this.vaultService.assertIsUnlocked();
-
const disposableKey = await this.localStorage.getDisposableSigner(signer);
// If disposable key is provided, use it for signing
@@ -721,15 +747,23 @@ export class KeyRing {
disposableKey.privateKey
: await this.getSigningKey(signer);
+ const { signing } = this.sdkService.getSdk();
+
+ return await signing.sign(txProps, key, chainId);
+ }
+
+ async signMasp(txProps: TxProps, signer: string): Promise {
+ await this.vaultService.assertIsUnlocked();
+
+ const disposableKey = await this.localStorage.getDisposableSigner(signer);
+ const realAddress = disposableKey?.realAddress || signer;
+
// If disposable key is provided, use it to map real address to spending key
- const spendingKeys =
- disposableKey ?
- [await this.getSpendingKey(disposableKey.realAddress)]
- : [];
+ const xsks = [await this.getSpendingKey(realAddress)];
const { signing } = this.sdkService.getSdk();
- return await signing.sign(txProps, key, spendingKeys, chainId);
+ return await signing.signMasp(txProps, xsks);
}
async signArbitrary(
@@ -748,10 +782,13 @@ export class KeyRing {
async queryAccountDetails(
address: string
): Promise {
+ const disposableKey = await this.localStorage.getDisposableSigner(address);
+
const account = await this.vaultStorage.findOneOrFail(
KeyStore,
"address",
- address
+ // if we use disposable key, we want to get the real address
+ disposableKey?.realAddress || address
);
if (!account) {
return;
diff --git a/apps/extension/src/background/keyring/messages.ts b/apps/extension/src/background/keyring/messages.ts
index 927657485d..f658183a94 100644
--- a/apps/extension/src/background/keyring/messages.ts
+++ b/apps/extension/src/background/keyring/messages.ts
@@ -186,27 +186,23 @@ export class AddLedgerAccountMsg extends Message {
public readonly address: string,
public readonly publicKey: string,
public readonly bip44Path: Bip44Path,
- public readonly parentId?: string
+ public readonly extendedViewingKey: string,
+ public readonly pseudoExtendedKey: string,
+ public readonly paymentAddress: string
) {
super();
}
validate(): void {
- if (!this.alias) {
- throw new Error("Alias must not be empty!");
- }
-
- if (!this.address) {
- throw new Error("Address was not provided!");
- }
-
- if (!this.publicKey) {
- throw new Error("Public key was not provided!");
- }
-
- if (!this.bip44Path) {
- throw new Error("BIP44 Path was not provided!");
- }
+ validateProps(this, [
+ "alias",
+ "address",
+ "publicKey",
+ "bip44Path",
+ "extendedViewingKey",
+ "pseudoExtendedKey",
+ "paymentAddress",
+ ]);
}
route(): string {
@@ -269,13 +265,7 @@ export class SetActiveAccountMsg extends Message {
}
validate(): void {
- if (!this.accountId) {
- throw new Error("Account ID is not set!");
- }
-
- if (!this.accountType) {
- throw new Error("Account Type is required!");
- }
+ validateProps(this, ["accountId", "accountType"]);
}
route(): string {
@@ -369,10 +359,7 @@ export class QueryAccountDetailsMsg extends Message<
}
validate(): void {
- if (!this.address) {
- throw new Error("Account address is required!");
- }
- return;
+ validateProps(this, ["address"]);
}
route(): string {
@@ -397,12 +384,7 @@ export class AppendLedgerSignatureMsg extends Message {
}
validate(): void {
- if (!this.txBytes) {
- throw new Error("txBytes is required!");
- }
- if (!this.signature) {
- throw new Error("signature is required!");
- }
+ validateProps(this, ["txBytes", "signature"]);
}
route(): string {
diff --git a/apps/extension/src/background/keyring/service.ts b/apps/extension/src/background/keyring/service.ts
index 4be1e92b53..cd4409f41c 100644
--- a/apps/extension/src/background/keyring/service.ts
+++ b/apps/extension/src/background/keyring/service.ts
@@ -81,7 +81,10 @@ export class KeyRingService {
alias: string,
address: string,
publicKey: string,
- bip44Path: Bip44Path
+ bip44Path: Bip44Path,
+ extendedViewingKey: string,
+ pseudoExtendedKey: string,
+ paymentAddress: string
): Promise {
const account = await this._keyRing.queryAccountByAddress(address);
if (account) {
@@ -94,7 +97,10 @@ export class KeyRingService {
alias,
address,
publicKey,
- bip44Path
+ bip44Path,
+ pseudoExtendedKey,
+ extendedViewingKey,
+ paymentAddress
);
await this.broadcaster.updateAccounts();
@@ -210,6 +216,10 @@ export class KeyRingService {
return await this._keyRing.sign(txProps, signer, chainId);
}
+ async signMasp(txProps: TxProps, signer: string): Promise {
+ return await this._keyRing.signMasp(txProps, signer);
+ }
+
async signArbitrary(
signer: string,
data: string
diff --git a/apps/extension/src/utils/index.ts b/apps/extension/src/utils/index.ts
index 12a5d5f208..fadb9da122 100644
--- a/apps/extension/src/utils/index.ts
+++ b/apps/extension/src/utils/index.ts
@@ -145,7 +145,7 @@ export const isShieldedPool = (address: string): boolean => {
*/
export const parseTransferType = (
tx: TransferProps,
- wrapperFeePayer?: string
+ wrapperFeePayer: string
): { source: string; target: string; type: TransferType } => {
const { sources, targets } = tx;
const source = sources[0].owner;
diff --git a/apps/extension/webpack.config.js b/apps/extension/webpack.config.js
index 6411a9b8cf..60db123fc2 100644
--- a/apps/extension/webpack.config.js
+++ b/apps/extension/webpack.config.js
@@ -91,7 +91,7 @@ const plugins = [
MANIFEST_PATH,
...(NODE_ENV === "development" && TARGET === "firefox" ?
[MANIFEST_V2_DEV_ONLY_PATH]
- : []),
+ : []),
],
output: {
fileName: "./manifest.json",
@@ -140,7 +140,7 @@ module.exports = {
devtool:
NODE_ENV === "development" && TARGET === "firefox" ?
"eval-source-map"
- : false,
+ : false,
entry: {
content: "./src/content",
background: "./src/background",
@@ -224,7 +224,7 @@ module.exports = {
hints: "warning",
maxAssetSize: 200000,
maxEntrypointSize: 400000,
- assetFilter: function (assetFilename) {
+ assetFilter: function(assetFilename) {
assetFilename.endsWith(".wasm");
},
},
diff --git a/apps/namadillo/public/config.toml b/apps/namadillo/public/config.toml
index 23dfc293be..29d5b71031 100644
--- a/apps/namadillo/public/config.toml
+++ b/apps/namadillo/public/config.toml
@@ -3,4 +3,3 @@
#rpc_url = ""
#masp_indexer_url = ""
#localnet_enabled = false
-
diff --git a/apps/namadillo/src/App/Masp/MaspShield.tsx b/apps/namadillo/src/App/Masp/MaspShield.tsx
index f1e620224d..5c0d4f208a 100644
--- a/apps/namadillo/src/App/Masp/MaspShield.tsx
+++ b/apps/namadillo/src/App/Masp/MaspShield.tsx
@@ -10,13 +10,14 @@ import {
import { allDefaultAccountsAtom } from "atoms/accounts";
import { namadaTransparentAssetsAtom } from "atoms/balance/atoms";
import { chainParametersAtom } from "atoms/chain/atoms";
+import { ledgerStatusDataAtom } from "atoms/ledger";
import { rpcUrlAtom } from "atoms/settings";
import BigNumber from "bignumber.js";
import { useTransactionActions } from "hooks/useTransactionActions";
import { useTransfer } from "hooks/useTransfer";
import { wallets } from "integrations";
import invariant from "invariant";
-import { useAtomValue } from "jotai";
+import { useAtom, useAtomValue } from "jotai";
import { createTransferDataFromNamada } from "lib/transactions";
import { useState } from "react";
import { generatePath, useNavigate, useSearchParams } from "react-router-dom";
@@ -33,11 +34,14 @@ export const MaspShield: React.FC = () => {
const rpcUrl = useAtomValue(rpcUrlAtom);
const chainParameters = useAtomValue(chainParametersAtom);
const defaultAccounts = useAtomValue(allDefaultAccountsAtom);
-
+ const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom);
const { data: availableAssets, isLoading: isLoadingAssets } = useAtomValue(
namadaTransparentAssetsAtom
);
-
+ const ledgerAccountInfo = ledgerStatus && {
+ deviceConnected: ledgerStatus.connected,
+ errorMessage: ledgerStatus.errorMessage,
+ };
const chainId = chainParameters.data?.chainId;
const sourceAddress = defaultAccounts.data?.find(
(account) => account.type !== AccountType.ShieldedKeys
@@ -120,6 +124,9 @@ export const MaspShield: React.FC = () => {
}
};
+ // We stop the ledger status check when the transfer is in progress
+ setLedgerStatusStop(isPerformingTransfer);
+
return (
@@ -143,6 +150,7 @@ export const MaspShield: React.FC = () => {
onChangeSelectedAsset,
amount: displayAmount,
onChangeAmount: setDisplayAmount,
+ ledgerAccountInfo,
}}
destination={{
chain: namadaChain as Chain,
diff --git a/apps/namadillo/src/App/Masp/MaspUnshield.tsx b/apps/namadillo/src/App/Masp/MaspUnshield.tsx
index 7f059839fd..af45d69f3b 100644
--- a/apps/namadillo/src/App/Masp/MaspUnshield.tsx
+++ b/apps/namadillo/src/App/Masp/MaspUnshield.tsx
@@ -10,13 +10,14 @@ import {
import { allDefaultAccountsAtom } from "atoms/accounts";
import { namadaShieldedAssetsAtom } from "atoms/balance/atoms";
import { chainParametersAtom } from "atoms/chain/atoms";
+import { ledgerStatusDataAtom } from "atoms/ledger/atoms";
import { rpcUrlAtom } from "atoms/settings";
import BigNumber from "bignumber.js";
import { useTransactionActions } from "hooks/useTransactionActions";
import { useTransfer } from "hooks/useTransfer";
import { wallets } from "integrations";
import invariant from "invariant";
-import { useAtomValue } from "jotai";
+import { useAtom, useAtomValue } from "jotai";
import { createTransferDataFromNamada } from "lib/transactions";
import { useState } from "react";
import { generatePath, useNavigate, useSearchParams } from "react-router-dom";
@@ -32,13 +33,17 @@ export const MaspUnshield: React.FC = () => {
const rpcUrl = useAtomValue(rpcUrlAtom);
const chainParameters = useAtomValue(chainParametersAtom);
const defaultAccounts = useAtomValue(allDefaultAccountsAtom);
-
+ const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom);
const { data: availableAssets, isLoading: isLoadingAssets } = useAtomValue(
namadaShieldedAssetsAtom
);
const { storeTransaction } = useTransactionActions();
+ const ledgerAccountInfo = ledgerStatus && {
+ deviceConnected: ledgerStatus.connected,
+ errorMessage: ledgerStatus.errorMessage,
+ };
const chainId = chainParameters.data?.chainId;
const account = defaultAccounts.data?.find(
(account) => account.type === AccountType.ShieldedKeys
@@ -119,6 +124,8 @@ export const MaspUnshield: React.FC = () => {
setGeneralErrorMessage(err + "");
}
};
+ // We stop the ledger status check when the transfer is in progress
+ setLedgerStatusStop(isPerformingTransfer);
return (
@@ -144,6 +151,7 @@ export const MaspUnshield: React.FC = () => {
onChangeSelectedAsset,
amount: displayAmount,
onChangeAmount: setDisplayAmount,
+ ledgerAccountInfo,
}}
destination={{
chain: namadaChain as Chain,
diff --git a/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx b/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx
index 201471f3f0..075b38f85a 100644
--- a/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx
+++ b/apps/namadillo/src/App/NamadaTransfer/NamadaTransfer.tsx
@@ -13,13 +13,14 @@ import {
namadaTransparentAssetsAtom,
} from "atoms/balance/atoms";
import { chainParametersAtom } from "atoms/chain/atoms";
+import { ledgerStatusDataAtom } from "atoms/ledger";
import { applicationFeaturesAtom, rpcUrlAtom } from "atoms/settings";
import BigNumber from "bignumber.js";
import { useTransactionActions } from "hooks/useTransactionActions";
import { useTransfer } from "hooks/useTransfer";
import { wallets } from "integrations";
import invariant from "invariant";
-import { useAtomValue } from "jotai";
+import { useAtom, useAtomValue } from "jotai";
import { createTransferDataFromNamada } from "lib/transactions";
import { useMemo, useState } from "react";
import { generatePath, useNavigate, useSearchParams } from "react-router-dom";
@@ -41,6 +42,7 @@ export const NamadaTransfer: React.FC = () => {
const features = useAtomValue(applicationFeaturesAtom);
const chainParameters = useAtomValue(chainParametersAtom);
const defaultAccounts = useAtomValue(allDefaultAccountsAtom);
+ const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom);
const { data: availableAssetsData, isLoading: isLoadingAssets } =
useAtomValue(
@@ -49,6 +51,10 @@ export const NamadaTransfer: React.FC = () => {
const { storeTransaction } = useTransactionActions();
+ const ledgerAccountInfo = ledgerStatus && {
+ deviceConnected: ledgerStatus.connected,
+ errorMessage: ledgerStatus.errorMessage,
+ };
const availableAssets = useMemo(() => {
if (features.namTransfersEnabled) {
return availableAssetsData;
@@ -152,6 +158,9 @@ export const NamadaTransfer: React.FC = () => {
}
};
+ // We stop the ledger status check when the transfer is in progress
+ setLedgerStatusStop(isPerformingTransfer);
+
return (
@@ -180,12 +189,14 @@ export const NamadaTransfer: React.FC = () => {
onChangeShielded: setShielded,
amount: displayAmount,
onChangeAmount: setDisplayAmount,
+ ledgerAccountInfo,
}}
destination={{
chain: namadaChain as Chain,
enableCustomAddress: true,
customAddress,
onChangeCustomAddress: setCustomAddress,
+ isShielded: isTargetShielded,
}}
feeProps={feeProps}
isSubmitting={isPerformingTransfer}
diff --git a/apps/namadillo/src/App/Transfer/TransferModule.tsx b/apps/namadillo/src/App/Transfer/TransferModule.tsx
index 413b4e997a..f3178b2644 100644
--- a/apps/namadillo/src/App/Transfer/TransferModule.tsx
+++ b/apps/namadillo/src/App/Transfer/TransferModule.tsx
@@ -11,6 +11,7 @@ import {
Address,
AddressWithAssetAndAmountMap,
GasConfig,
+ LedgerAccountInfo,
WalletProvider,
} from "types";
import { getDisplayGasFee } from "utils/gas";
@@ -34,6 +35,8 @@ type TransferModuleConfig = {
onChangeChain?: (chain: Chain) => void;
isShielded?: boolean;
onChangeShielded?: (isShielded: boolean) => void;
+ // Additional information if selected account is a ledger
+ ledgerAccountInfo?: LedgerAccountInfo;
};
export type TransferSourceProps = TransferModuleConfig & {
@@ -92,6 +95,7 @@ type ValidationResult =
| "NoDestinationChain"
| "NoTransactionFee"
| "NotEnoughBalance"
+ | "NoLedgerConnected"
| "Ok";
export const TransferModule = ({
@@ -177,6 +181,12 @@ export const TransferModule = ({
return "NotEnoughBalance";
} else if (!destination.wallet && !destination.customAddress) {
return "NoDestinationWallet";
+ } else if (
+ (source.isShielded || destination.isShielded) &&
+ source.ledgerAccountInfo &&
+ !source.ledgerAccountInfo.deviceConnected
+ ) {
+ return "NoLedgerConnected";
} else {
return "Ok";
}
@@ -262,7 +272,6 @@ export const TransferModule = ({
case "NoSelectedAsset":
return getText("Select Asset");
-
case "NoDestinationWallet":
return getText("Select Destination Wallet");
@@ -274,6 +283,9 @@ export const TransferModule = ({
case "NotEnoughBalance":
return getText("Not enough balance");
+
+ case "NoLedgerConnected":
+ return getText("Connect your ledger and open the Namada App");
}
if (!availableAmountMinusFees) {
diff --git a/apps/namadillo/src/atoms/accounts/atoms.ts b/apps/namadillo/src/atoms/accounts/atoms.ts
index c4c0af8816..6e359ceeac 100644
--- a/apps/namadillo/src/atoms/accounts/atoms.ts
+++ b/apps/namadillo/src/atoms/accounts/atoms.ts
@@ -10,6 +10,7 @@ import { namadaExtensionConnectedAtom } from "atoms/settings";
import { queryDependentFn } from "atoms/utils";
import BigNumber from "bignumber.js";
import { NamadaKeychain } from "hooks/useNamadaKeychain";
+import { atom } from "jotai";
import { atomWithMutation, atomWithQuery } from "jotai-tanstack-query";
import {
fetchAccountBalance,
@@ -69,6 +70,13 @@ export const allDefaultAccountsAtom = atomWithQuery((get) => {
};
});
+export const isLedgerAccountAtom = atom((get) => {
+ const defaultAccounts = get(allDefaultAccountsAtom);
+ return Boolean(
+ defaultAccounts.data?.find((account) => account.type === AccountType.Ledger)
+ );
+});
+
export const updateDefaultAccountAtom = atomWithMutation(() => {
const namadaPromise = new NamadaKeychain().get();
return {
diff --git a/apps/namadillo/src/atoms/ledger/atoms.ts b/apps/namadillo/src/atoms/ledger/atoms.ts
new file mode 100644
index 0000000000..c82f91f6e9
--- /dev/null
+++ b/apps/namadillo/src/atoms/ledger/atoms.ts
@@ -0,0 +1,75 @@
+import { ledgerUSBList } from "@namada/sdk/web";
+import { LedgerError } from "@zondax/ledger-namada";
+import { isLedgerAccountAtom } from "atoms/accounts";
+import { atom } from "jotai";
+
+import { atomWithQuery } from "jotai-tanstack-query";
+import { getSdkInstance } from "utils/sdk";
+
+export type LedgerStatus = {
+ connected: boolean;
+ errorMessage: string;
+};
+
+const ledgerStatusStopAtom = atom(false);
+
+export const ledgerStatusAtom = atomWithQuery(() => {
+ return {
+ refetchInterval: 1000,
+ queryKey: ["ledger-status"],
+ queryFn: async () => {
+ const devices = await ledgerUSBList();
+
+ if (devices.length > 0) {
+ try {
+ // Disable console.warn to prevent the warning from showing up in the UI in the loop
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (console as any).warnOld = console.warn;
+ console.warn = () => {};
+
+ const sdk = await getSdkInstance();
+ const ledger = await sdk.initLedger();
+ const {
+ version: { returnCode, errorMessage },
+ } = await ledger.status();
+
+ const connected = returnCode === LedgerError.NoErrors;
+
+ await ledger.closeTransport();
+
+ return {
+ connected,
+ errorMessage,
+ };
+ } catch (e) {
+ return {
+ connected: false,
+ errorMessage: `${e}`,
+ };
+ } finally {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ console.warn = (console as any).warnOld;
+ }
+ }
+
+ return {
+ connected: false,
+ errorMessage: "Ledger device not detected",
+ };
+ },
+ };
+});
+
+export const ledgerStatusDataAtom = atom(
+ (get) => {
+ const isLedgerAccount = get(isLedgerAccountAtom);
+ const ledgetStatusStop = get(ledgerStatusStopAtom);
+
+ if (isLedgerAccount && !ledgetStatusStop) {
+ return get(ledgerStatusAtom).data;
+ }
+ },
+ (_, set, stop: boolean) => {
+ set(ledgerStatusStopAtom, stop);
+ }
+);
diff --git a/apps/namadillo/src/atoms/ledger/index.ts b/apps/namadillo/src/atoms/ledger/index.ts
new file mode 100644
index 0000000000..4e0d46d9aa
--- /dev/null
+++ b/apps/namadillo/src/atoms/ledger/index.ts
@@ -0,0 +1 @@
+export * from "./atoms";
diff --git a/apps/namadillo/src/atoms/transfer/services.ts b/apps/namadillo/src/atoms/transfer/services.ts
index f86184ce0d..09e6f62deb 100644
--- a/apps/namadillo/src/atoms/transfer/services.ts
+++ b/apps/namadillo/src/atoms/transfer/services.ts
@@ -1,5 +1,7 @@
import {
Account,
+ AccountType,
+ BparamsMsgValue,
GenDisposableSignerResponse,
ShieldedTransferMsgValue,
ShieldedTransferProps,
@@ -95,11 +97,21 @@ export const createShieldedTransferTx = async (
disposableSigner: GenDisposableSignerResponse,
memo?: string
): Promise | undefined> => {
+ const { publicKey: signerPublicKey } = disposableSigner;
const source = props[0]?.data[0]?.source;
const destination = props[0]?.data[0]?.target;
const token = props[0]?.data[0]?.token;
const amount = props[0]?.data[0]?.amount;
+ let bparams: BparamsMsgValue[] | undefined;
+
+ if (account.type === AccountType.Ledger) {
+ const sdk = await getSdkInstance();
+ const ledger = await sdk.initLedger();
+ bparams = await ledger.getBparams();
+ ledger.closeTransport();
+ }
+
return await workerBuildTxPair({
rpcUrl,
token,
@@ -107,13 +119,14 @@ export const createShieldedTransferTx = async (
const msgValue = new ShieldedTransferMsgValue({
gasSpendingKey: source,
data: [{ source, target: destination, token, amount }],
+ bparams,
});
const msg: ShieldedTransfer = {
type: "shielded-transfer",
payload: {
account: {
...account,
- publicKey: disposableSigner.publicKey,
+ publicKey: signerPublicKey,
},
gasConfig,
props: [msgValue],
@@ -142,6 +155,15 @@ export const createShieldingTransferTx = async (
const token = props[0]?.data[0]?.token;
const amount = props[0]?.data[0]?.amount;
+ let bparams: BparamsMsgValue[] | undefined;
+
+ if (account.type === AccountType.Ledger) {
+ const sdk = await getSdkInstance();
+ const ledger = await sdk.initLedger();
+ bparams = await ledger.getBparams();
+ ledger.closeTransport();
+ }
+
return await workerBuildTxPair({
rpcUrl,
token,
@@ -150,6 +172,7 @@ export const createShieldingTransferTx = async (
const msgValue = new ShieldingTransferMsgValue({
target: destination,
data: [{ source, token, amount }],
+ bparams,
});
const msg: Shield = {
type: "shield",
@@ -179,11 +202,22 @@ export const createUnshieldingTransferTx = async (
disposableSigner: GenDisposableSignerResponse,
memo?: string
): Promise | undefined> => {
+ const { publicKey: signerPublicKey } = disposableSigner;
+
const source = props[0]?.source;
const destination = props[0]?.data[0]?.target;
const token = props[0]?.data[0]?.token;
const amount = props[0]?.data[0]?.amount;
+ let bparams: BparamsMsgValue[] | undefined;
+
+ if (account.type === AccountType.Ledger) {
+ const sdk = await getSdkInstance();
+ const ledger = await sdk.initLedger();
+ bparams = await ledger.getBparams();
+ ledger.closeTransport();
+ }
+
return await workerBuildTxPair({
rpcUrl,
token,
@@ -192,13 +226,14 @@ export const createUnshieldingTransferTx = async (
source,
gasSpendingKey: source,
data: [{ target: destination, token, amount }],
+ bparams,
});
const msg: Unshield = {
type: "unshield",
payload: {
account: {
...account,
- publicKey: disposableSigner.publicKey,
+ publicKey: signerPublicKey,
},
gasConfig,
props: [msgValue],
diff --git a/apps/namadillo/src/types.ts b/apps/namadillo/src/types.ts
index e6f90231d7..34a8e12a10 100644
--- a/apps/namadillo/src/types.ts
+++ b/apps/namadillo/src/types.ts
@@ -386,3 +386,8 @@ export type LocalnetToml = {
chain_1_channel: string;
chain_2_channel: string;
};
+
+export type LedgerAccountInfo = {
+ deviceConnected: boolean;
+ errorMessage: string;
+};
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 0156c679d0..e72718615c 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -60,11 +60,11 @@
"@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",
+ "semver": "^7.6.3",
"slip44": "^3.0.18"
},
"devDependencies": {
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index cd7c35ece4..627c840b48 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -1,13 +1,10 @@
// Make Ledger available for direct-import as it is not dependent on Sdk initialization
-export {
- Ledger,
- initLedgerHIDTransport,
- initLedgerUSBTransport,
-} from "./ledger";
+export { Ledger, initLedgerUSBTransport, ledgerUSBList } from "./ledger";
export type {
LedgerAddressAndPublicKey,
- LedgerShieldedKeys,
+ LedgerProofGenerationKey,
LedgerStatus,
+ LedgerViewingKey,
} from "./ledger";
// Export types
@@ -37,6 +34,11 @@ export { ProgressBarNames, Sdk, SdkEvents } from "./sdk";
export { publicKeyToBech32 } from "./keys";
+export {
+ ExtendedViewingKey,
+ ProofGenerationKey,
+ PseudoExtendedKey,
+} from "./masp";
export type { Masp } from "./masp";
export { PhraseSize } from "./mnemonic";
export type { Mnemonic } from "./mnemonic";
diff --git a/packages/sdk/src/ledger.ts b/packages/sdk/src/ledger.ts
index e4ad28ee40..e2ee314b70 100644
--- a/packages/sdk/src/ledger.ts
+++ b/packages/sdk/src/ledger.ts
@@ -1,5 +1,4 @@
import Transport from "@ledgerhq/hw-transport";
-import TransportHID from "@ledgerhq/hw-transport-webhid";
import TransportUSB from "@ledgerhq/hw-transport-webusb";
import { chains } from "@namada/chains";
import {
@@ -12,21 +11,18 @@ import {
ResponseVersion,
ResponseViewKey,
} from "@zondax/ledger-namada";
-import { makeBip44Path } from "./utils";
+import semver from "semver";
+import { makeBip44Path, makeSaplingPath } from "./utils";
const { coinType } = chains.namada.bip44;
export type LedgerAddressAndPublicKey = { address: string; publicKey: string };
-export type LedgerShieldedKeys = {
- viewingKey: {
- viewKey?: string;
- ivk?: string;
- ovk?: string;
- };
- proofGenerationKey: {
- ak?: string;
- nsk?: string;
- };
+export type LedgerViewingKey = {
+ xfvk: Uint8Array;
+};
+export type LedgerProofGenerationKey = {
+ ak: Uint8Array;
+ nsk: Uint8Array;
};
export type LedgerStatus = {
@@ -34,6 +30,22 @@ export type LedgerStatus = {
info: ResponseAppInfo;
};
+const LEDGER_MIN_VERSION_ZIP32 = "2.0.0";
+
+export type Bparams = {
+ spend: {
+ rcv: Uint8Array;
+ alpha: Uint8Array;
+ };
+ output: {
+ rcv: Uint8Array;
+ rcm: Uint8Array;
+ };
+ convert: {
+ rcv: Uint8Array;
+ };
+};
+
/**
* Initialize USB transport
* @async
@@ -44,20 +56,23 @@ export const initLedgerUSBTransport = async (): Promise => {
};
/**
- * Initialize HID transport
+ * Returns a list of ledger devices
* @async
- * @returns Transport object
+ * @returns List of USB devices
*/
-export const initLedgerHIDTransport = async (): Promise => {
- return await TransportHID.create();
+export const ledgerUSBList = async (): Promise => {
+ return await TransportUSB.list();
};
-
export const DEFAULT_LEDGER_BIP44_PATH = makeBip44Path(coinType, {
account: 0,
change: 0,
index: 0,
});
+export const DEFAULT_LEDGER_ZIP32_PATH = makeSaplingPath(coinType, {
+ account: 0,
+});
+
/**
* Functionality for interacting with NamadaApp for Ledger Hardware Wallets
*/
@@ -65,7 +80,7 @@ export class Ledger {
/**
* @param namadaApp - Inititalized NamadaApp class from Zondax package
*/
- private constructor(public readonly namadaApp: NamadaApp) {}
+ private constructor(public readonly namadaApp: NamadaApp) { }
/**
* Initialize and return Ledger class instance with initialized Transport
@@ -147,20 +162,103 @@ export class Ledger {
}
/**
- * Prompt user to get viewing and proof gen key associated with optional path, otherwise, use default path.
- * Throw exception if app is not initialized.
+ * Get Bparams for masp transactions
* @async
- * @param [path] Bip44 path for deriving key
+ * @returns bparams
+ */
+ public async getBparams(): Promise {
+ // We need to clean the randomness buffers before getting randomness
+ // to ensure that the randomness is not reused
+ await this.namadaApp.cleanRandomnessBuffers();
+ const results: Bparams[] = [];
+ let tries = 0;
+
+ // This should not happen usually, but in case some of the responses are not valid, we will retry.
+ // 15 is a maximum number of spend/output/convert description randomness parameters that can be
+ // generated on the hardware wallet. This also means that ledger can sign maximum of 15 spend, output
+ // and convert descriptions in one tx.
+ while (results.length < 15) {
+ tries++;
+ if (tries === 20) {
+ throw new Error("Could not get valid Bparams, too many tries");
+ }
+
+ const spend_response = await this.namadaApp.getSpendRandomness();
+ const output_response = await this.namadaApp.getOutputRandomness();
+ const convert_response = await this.namadaApp.getConvertRandomness();
+ if (
+ spend_response.returnCode !== LedgerError.NoErrors ||
+ output_response.returnCode !== LedgerError.NoErrors ||
+ convert_response.returnCode !== LedgerError.NoErrors
+ ) {
+ continue;
+ }
+
+ results.push({
+ spend: {
+ rcv: spend_response.rcv,
+ alpha: spend_response.alpha,
+ },
+ output: {
+ rcv: output_response.rcv,
+ rcm: output_response.rcm,
+ },
+ convert: {
+ rcv: convert_response.rcv,
+ },
+ });
+ }
+
+ return results;
+ }
+
+ /**
+ * Prompt user to get viewing key associated with optional path, otherwise, use default path.
+ * Throw exception if app is not initialized, zip32 is not supported, or key is not returned.
+ * @async
+ * @param [path] Zip32 path for deriving key
* @param [promptUser] boolean to determine whether to display on Ledger device and require approval
* @returns ShieldedKeys
*/
- public async getShieldedKeys(
- path: string = DEFAULT_LEDGER_BIP44_PATH,
+ public async getViewingKey(
+ path: string = DEFAULT_LEDGER_ZIP32_PATH,
promptUser = true
- ): Promise {
+ ): Promise {
try {
- const { viewKey, ivk, ovk }: ResponseViewKey =
- await this.namadaApp.retrieveKeys(path, NamadaKeys.ViewKey, promptUser);
+ await this.validateVersionForZip32();
+
+ const { xfvk }: ResponseViewKey = await this.namadaApp.retrieveKeys(
+ path,
+ NamadaKeys.ViewKey,
+ promptUser
+ );
+
+ if (!xfvk) {
+ throw new Error("Did not receive viewing key!");
+ }
+
+ return {
+ xfvk: new Uint8Array(xfvk),
+ };
+ } catch (e) {
+ throw new Error(`${e}`);
+ }
+ }
+
+ /**
+ * Prompt user to get proof generation key associated with optional path, otherwise, use default path.
+ * Throw exception if app is not initialized, zip32 is not supported, or key is not returned.
+ * @async
+ * @param [path] Zip32 path for deriving key
+ * @param [promptUser] boolean to determine whether to display on Ledger device and require approval
+ * @returns ShieldedKeys
+ */
+ public async getProofGenerationKey(
+ path: string = DEFAULT_LEDGER_ZIP32_PATH,
+ promptUser = true
+ ): Promise {
+ try {
+ await this.validateVersionForZip32();
const { ak, nsk }: ResponseProofGenKey =
await this.namadaApp.retrieveKeys(
@@ -169,19 +267,16 @@ export class Ledger {
promptUser
);
+ if (!ak || !nsk) {
+ throw new Error("Did not receive proof generation key!");
+ }
+
return {
- viewingKey: {
- viewKey: viewKey?.toString(),
- ivk: ivk?.toString(),
- ovk: ovk?.toString(),
- },
- proofGenerationKey: {
- ak: ak?.toString(),
- nsk: nsk?.toString(),
- },
+ ak: new Uint8Array(ak),
+ nsk: new Uint8Array(nsk),
};
- } catch (_) {
- throw new Error(`Could not retrieve Viewing Key`);
+ } catch (e) {
+ throw new Error(`${e}`);
}
}
@@ -228,4 +323,35 @@ export class Ledger {
public async closeTransport(): Promise {
return await this.namadaApp.transport.close();
}
+
+ /**
+ * Check if Zip32 is supported by the installed app's version.
+ * Throws error if app is not initialized
+ * @async
+ * @returns boolean
+ */
+ public async isZip32Supported(): Promise {
+ const {
+ info: { appVersion },
+ } = await this.status();
+ return !semver.lt(appVersion, LEDGER_MIN_VERSION_ZIP32);
+ }
+
+ /**
+ * Validate the version against the minimum required version for Zip32 functionality.
+ * Throw error if it is unsupported or app is not initialized.
+ * @async
+ * @returns void
+ */
+ private async validateVersionForZip32(): Promise {
+ if (!(await this.isZip32Supported())) {
+ const {
+ info: { appVersion },
+ } = await this.status();
+ throw new Error(
+ `This method requires Zip32 and is unsupported in ${appVersion}! ` +
+ `Please update to at least ${LEDGER_MIN_VERSION_ZIP32}!`
+ );
+ }
+ }
}
diff --git a/packages/sdk/src/masp/index.ts b/packages/sdk/src/masp/index.ts
new file mode 100644
index 0000000000..9a8f1db3cf
--- /dev/null
+++ b/packages/sdk/src/masp/index.ts
@@ -0,0 +1,2 @@
+export * from "./masp";
+export * from "./types";
diff --git a/packages/sdk/src/masp.ts b/packages/sdk/src/masp/masp.ts
similarity index 100%
rename from packages/sdk/src/masp.ts
rename to packages/sdk/src/masp/masp.ts
diff --git a/packages/sdk/src/masp/types.ts b/packages/sdk/src/masp/types.ts
new file mode 100644
index 0000000000..1123ea2cbd
--- /dev/null
+++ b/packages/sdk/src/masp/types.ts
@@ -0,0 +1,5 @@
+export {
+ ExtendedViewingKey,
+ ProofGenerationKey,
+ PseudoExtendedKey,
+} from "@namada/shared";
diff --git a/packages/sdk/src/signing.ts b/packages/sdk/src/signing.ts
index ca9560bf66..0ede34e434 100644
--- a/packages/sdk/src/signing.ts
+++ b/packages/sdk/src/signing.ts
@@ -11,30 +11,23 @@ export class Signing {
* Signing constructor
* @param sdk - Instance of Sdk struct from wasm lib
*/
- constructor(protected readonly sdk: SdkWasm) {}
+ constructor(protected readonly sdk: SdkWasm) { }
/**
* Sign Namada transaction
* @param txProps - TxProps
* @param signingKey - private key(s)
- * @param xsks - spending keys
* @param [chainId] - optional chain ID, will enforce validation if present
* @returns signed tx bytes - Promise resolving to Uint8Array
*/
async sign(
txProps: TxProps,
signingKey: string | string[],
- xsks?: string[],
chainId?: string
): Promise {
const txMsgValue = new TxMsgValue(txProps);
const msg = new Message();
const txBytes = msg.encode(txMsgValue);
- const txBytesFinal =
- xsks && xsks.length > 0 ?
- await this.sdk.sign_masp(xsks, txBytes)
- : txBytes;
-
let signingKeys: string[] = [];
if (signingKey instanceof Array) {
@@ -43,7 +36,21 @@ export class Signing {
signingKeys.push(signingKey);
}
- return await this.sdk.sign_tx(txBytesFinal, signingKeys, chainId);
+ return await this.sdk.sign_tx(txBytes, signingKeys, chainId);
+ }
+
+ /**
+ * Sign masp spends
+ * @param txProps - TxProps
+ * @param xsks - spending keys
+ * @returns tx with masp spends signed - Promise resolving to Uint8Array
+ */
+ async signMasp(txProps: TxProps, xsks: string[]): Promise {
+ const txMsgValue = new TxMsgValue(txProps);
+ const msg = new Message();
+ const txBytes = msg.encode(txMsgValue);
+
+ return await this.sdk.sign_masp(xsks, txBytes);
}
/**
diff --git a/packages/sdk/src/tests/ledger.test.ts b/packages/sdk/src/tests/ledger.test.ts
index 3aa9969cca..0934e984f2 100644
--- a/packages/sdk/src/tests/ledger.test.ts
+++ b/packages/sdk/src/tests/ledger.test.ts
@@ -1,13 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import TransportHID from "@ledgerhq/hw-transport-webhid";
import TransportUSB from "@ledgerhq/hw-transport-webusb";
import * as LedgerNamadaNS from "@zondax/ledger-namada";
import * as LedgerNS from "../ledger";
-import {
- Ledger,
- initLedgerHIDTransport,
- initLedgerUSBTransport,
-} from "../ledger";
+import { Ledger, initLedgerUSBTransport } from "../ledger";
// Needed otherwise we can't redefine the classes from this module
jest.mock("@zondax/ledger-namada", () => {
@@ -37,17 +32,6 @@ describe("ledger", () => {
});
});
- describe("initLedgerHIDTransport", () => {
- it("should initialize a Ledger HID transport", async () => {
- const returned = { hid: true };
- jest.spyOn(TransportHID, "create").mockResolvedValue(returned as any);
- const res = await initLedgerHIDTransport();
-
- expect(TransportHID.create).toHaveBeenCalled();
- expect(res).toEqual(returned);
- });
- });
-
describe("Ledger", () => {
describe("init", () => {
it("should initialize a ledger with provided Transport", async () => {
diff --git a/packages/sdk/src/tx/tx.ts b/packages/sdk/src/tx/tx.ts
index fedaf3289c..67fdb6ed28 100644
--- a/packages/sdk/src/tx/tx.ts
+++ b/packages/sdk/src/tx/tx.ts
@@ -366,6 +366,21 @@ export class Tx {
return deserialize(Buffer.from(batch), TxMsgValue);
}
+ /**
+ * Append signature for transactions signed by Ledger Hardware Wallet
+ * @param txBytes - bytes of the transaction
+ * @param signingData - signing data
+ * @param signature - masp signature
+ * @returns transaction bytes with signature appended
+ */
+ appendMaspSignature(
+ txBytes: Uint8Array,
+ signingData: Uint8Array[],
+ signature: Uint8Array
+ ): Uint8Array {
+ return this.sdk.sign_masp_ledger(txBytes, signingData, signature);
+ }
+
/**
* Append signature for transactions signed by Ledger Hardware Wallet
* @param txBytes - Serialized transaction
diff --git a/packages/shared/lib/src/sdk/args.rs b/packages/shared/lib/src/sdk/args.rs
index 15bc829bad..e4750d5577 100644
--- a/packages/shared/lib/src/sdk/args.rs
+++ b/packages/shared/lib/src/sdk/args.rs
@@ -516,6 +516,34 @@ pub fn transparent_transfer_tx_args(
Ok(args)
}
+#[derive(BorshSerialize, BorshDeserialize, Debug)]
+#[borsh(crate = "namada_sdk::borsh")]
+pub struct BparamsSpendMsg {
+ rcv: Vec,
+ alpha: Vec,
+}
+
+#[derive(BorshSerialize, BorshDeserialize, Debug)]
+#[borsh(crate = "namada_sdk::borsh")]
+pub struct BparamsOutputMsg {
+ rcv: Vec,
+ rcm: Vec,
+}
+
+#[derive(BorshSerialize, BorshDeserialize, Debug)]
+#[borsh(crate = "namada_sdk::borsh")]
+pub struct BparamsConvertMsg {
+ rcv: Vec,
+}
+
+#[derive(BorshSerialize, BorshDeserialize, Debug)]
+#[borsh(crate = "namada_sdk::borsh")]
+pub struct BparamsMsg {
+ spend: BparamsSpendMsg,
+ output: BparamsOutputMsg,
+ convert: BparamsConvertMsg,
+}
+
#[derive(BorshSerialize, BorshDeserialize, Debug)]
#[borsh(crate = "namada_sdk::borsh")]
pub struct ShieldedTransferDataMsg {
@@ -530,6 +558,7 @@ pub struct ShieldedTransferDataMsg {
pub struct ShieldedTransferMsg {
data: Vec,
gas_spending_key: Option,
+ bparams: Option>,
}
/// Maps serialized tx_msg into TxShieldedTransfer args.
@@ -546,11 +575,12 @@ pub struct ShieldedTransferMsg {
pub fn shielded_transfer_tx_args(
shielded_transfer_msg: &[u8],
tx_msg: &[u8],
-) -> Result {
+) -> Result<(args::TxShieldedTransfer, Option), JsError> {
let shielded_transfer_msg = ShieldedTransferMsg::try_from_slice(shielded_transfer_msg)?;
let ShieldedTransferMsg {
data,
gas_spending_key,
+ bparams: bparams_msg,
} = shielded_transfer_msg;
let gas_spending_key = gas_spending_key.map(|v| PseudoExtendedKey::decode(v).0);
@@ -574,6 +604,7 @@ pub fn shielded_transfer_tx_args(
}
let tx = tx_msg_into_args(tx_msg)?;
+ let bparams = bparams_msg_into_bparams(bparams_msg);
let args = args::TxShieldedTransfer {
data: shielded_transfer_data,
@@ -584,7 +615,7 @@ pub fn shielded_transfer_tx_args(
gas_spending_key,
};
- Ok(args)
+ Ok((args, bparams))
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
@@ -600,6 +631,7 @@ pub struct ShieldingTransferDataMsg {
pub struct ShieldingTransferMsg {
target: String,
data: Vec,
+ bparams: Option>,
}
/// Maps serialized tx_msg into TxShieldingTransfer args.
@@ -616,9 +648,13 @@ pub struct ShieldingTransferMsg {
pub fn shielding_transfer_tx_args(
shielding_transfer_msg: &[u8],
tx_msg: &[u8],
-) -> Result {
+) -> Result<(args::TxShieldingTransfer, Option), JsError> {
let shielding_transfer_msg = ShieldingTransferMsg::try_from_slice(shielding_transfer_msg)?;
- let ShieldingTransferMsg { target, data } = shielding_transfer_msg;
+ let ShieldingTransferMsg {
+ target,
+ data,
+ bparams: bparams_msg,
+ } = shielding_transfer_msg;
let target = PaymentAddress::from_str(&target)?;
let mut shielding_transfer_data: Vec = vec![];
@@ -638,6 +674,7 @@ pub fn shielding_transfer_tx_args(
}
let tx = tx_msg_into_args(tx_msg)?;
+ let bparams = bparams_msg_into_bparams(bparams_msg);
let args = args::TxShieldingTransfer {
data: shielding_transfer_data,
@@ -646,7 +683,7 @@ pub fn shielding_transfer_tx_args(
tx_code_path: PathBuf::from("tx_transfer.wasm"),
};
- Ok(args)
+ Ok((args, bparams))
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
@@ -663,8 +700,8 @@ pub struct UnshieldingTransferMsg {
source: String,
data: Vec,
gas_spending_key: Option,
+ bparams: Option>,
}
-
/// Maps serialized tx_msg into TxUnshieldingTransfer args.
///
/// # Arguments
@@ -679,13 +716,14 @@ pub struct UnshieldingTransferMsg {
pub fn unshielding_transfer_tx_args(
unshielding_transfer_msg: &[u8],
tx_msg: &[u8],
-) -> Result {
+) -> Result<(args::TxUnshieldingTransfer, Option), JsError> {
let unshielding_transfer_msg =
UnshieldingTransferMsg::try_from_slice(unshielding_transfer_msg)?;
let UnshieldingTransferMsg {
source,
data,
gas_spending_key,
+ bparams: bparams_msg,
} = unshielding_transfer_msg;
let source = PseudoExtendedKey::decode(source).0;
let gas_spending_key = gas_spending_key.map(|v| PseudoExtendedKey::decode(v).0);
@@ -706,6 +744,7 @@ pub fn unshielding_transfer_tx_args(
}
let tx = tx_msg_into_args(tx_msg)?;
+ let bparams = bparams_msg_into_bparams(bparams_msg);
let args = args::TxUnshieldingTransfer {
data: unshielding_transfer_data,
@@ -717,7 +756,7 @@ pub fn unshielding_transfer_tx_args(
tx_code_path: PathBuf::from("tx_transfer.wasm"),
};
- Ok(args)
+ Ok((args, bparams))
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
@@ -989,26 +1028,11 @@ fn tx_msg_into_args(tx_msg: &[u8]) -> Result {
pub enum BuildParams {
RngBuildParams(RngBuildParams),
- // TODO: HD Wallet support
- #[allow(dead_code)]
StoredBuildParams(StoredBuildParams),
}
-pub async fn generate_masp_build_params(
- // TODO: those will be needed for HD Wallet support
- _spend_len: usize,
- _convert_len: usize,
- _output_len: usize,
- args: &args::Tx,
-) -> Result {
- // Construct the build parameters that parameterized the Transaction
- // authorizations
- if args.use_device {
- // HD Wallet support
- Err(error::Error::Other("Device not supported".into()))
- } else {
- Ok(BuildParams::RngBuildParams(RngBuildParams::new(OsRng)))
- }
+pub fn generate_rng_build_params() -> BuildParams {
+ BuildParams::RngBuildParams(RngBuildParams::new(OsRng))
}
// Sign the given transaction's MASP component using real signatures
@@ -1075,7 +1099,50 @@ where
Ok(())
}
-struct MapSaplingSigAuth(HashMap::AuthSig>);
+fn bparams_msg_into_bparams(bparams_msg: Option>) -> Option {
+ bparams_msg.map(|bparams_msg| {
+ let mut bparams = StoredBuildParams::default();
+ for bpm in bparams_msg {
+ bparams
+ .spend_params
+ .push(sapling::builder::SpendBuildParams {
+ rcv: masp_primitives::jubjub::Fr::from_bytes(
+ &bpm.spend.rcv.try_into().unwrap(),
+ )
+ .unwrap(),
+ alpha: masp_primitives::jubjub::Fr::from_bytes(
+ &bpm.spend.alpha.try_into().unwrap(),
+ )
+ .unwrap(),
+ });
+
+ bparams
+ .output_params
+ .push(sapling::builder::OutputBuildParams {
+ rcv: masp_primitives::jubjub::Fr::from_bytes(
+ &bpm.output.rcv.try_into().unwrap(),
+ )
+ .unwrap(),
+ rseed: bpm.output.rcm.try_into().unwrap(),
+ ..sapling::builder::OutputBuildParams::default()
+ });
+
+ bparams
+ .convert_params
+ .push(sapling::builder::ConvertBuildParams {
+ rcv: masp_primitives::jubjub::Fr::from_bytes(
+ &bpm.convert.rcv.try_into().unwrap(),
+ )
+ .unwrap(),
+ });
+ }
+ bparams
+ })
+}
+
+pub struct MapSaplingSigAuth(
+ pub HashMap::AuthSig>,
+);
impl sapling::MapAuth for MapSaplingSigAuth {
fn map_proof(
diff --git a/packages/shared/lib/src/sdk/mod.rs b/packages/shared/lib/src/sdk/mod.rs
index cc02aa8243..8bd4d55e84 100644
--- a/packages/shared/lib/src/sdk/mod.rs
+++ b/packages/shared/lib/src/sdk/mod.rs
@@ -13,13 +13,15 @@ use crate::utils::set_panic_hook;
#[cfg(feature = "web")]
use crate::utils::to_bytes;
use crate::utils::to_js_result;
-use args::{generate_masp_build_params, masp_sign, BuildParams};
+use args::{generate_rng_build_params, masp_sign, BuildParams, MapSaplingSigAuth};
use gloo_utils::format::JsValueSerdeExt;
+use js_sys::Uint8Array;
use namada_sdk::address::{Address, ImplicitAddress, MASP};
use namada_sdk::args::{
GenIbcShieldingTransfer, IbcShieldingTransferAsset, InputAmount, Query, TxExpiration,
};
use namada_sdk::borsh::{self, BorshDeserialize};
+use namada_sdk::collections::HashMap;
use namada_sdk::eth_bridge::bridge_pool::build_bridge_pool_tx;
use namada_sdk::hash::Hash;
use namada_sdk::ibc::convert_masp_tx_to_ibc_memo;
@@ -29,7 +31,7 @@ use namada_sdk::key::{common, ed25519, RefTo, SigScheme};
use namada_sdk::masp::shielded_wallet::ShieldedApi;
use namada_sdk::masp::ShieldedContext;
use namada_sdk::masp_primitives::transaction::components::{
- amount::I128Sum, sapling::fees::InputView,
+ amount::I128Sum, sapling::builder::StoredBuildParams, sapling::fees::InputView,
};
use namada_sdk::masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedKey};
use namada_sdk::rpc::query_denom;
@@ -40,6 +42,7 @@ use namada_sdk::tendermint_rpc::Url;
use namada_sdk::token::{Amount, DenominatedAmount, MaspEpoch};
use namada_sdk::token::{MaspTxId, OptionExt};
use namada_sdk::tx::data::TxType;
+use namada_sdk::tx::Section;
use namada_sdk::tx::{
build_batch, build_bond, build_claim_rewards, build_ibc_transfer, build_redelegation,
build_reveal_pk, build_shielded_transfer, build_shielding_transfer, build_transparent_transfer,
@@ -54,22 +57,6 @@ use std::str::FromStr;
use tx::MaspSigningData;
use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
-// Maximum number of spend description randomness parameters that can be
-// generated on the hardware wallet. It is hard to compute the exact required
-// number because a given MASP source could be distributed amongst several
-// notes.
-const MAX_HW_SPEND: usize = 15;
-// Maximum number of convert description randomness parameters that can be
-// generated on the hardware wallet. It is hard to compute the exact required
-// number because the number of conversions that are used depends on the
-// protocol's current state.
-const MAX_HW_CONVERT: usize = 15;
-// Maximum number of output description randomness parameters that can be
-// generated on the hardware wallet. It is hard to compute the exact required
-// number because the number of outputs depends on the number of dummy outputs
-// introduced.
-const MAX_HW_OUTPUT: usize = 15;
-
/// Represents the Sdk public API.
#[wasm_bindgen]
pub struct Sdk {
@@ -153,12 +140,12 @@ impl Sdk {
pub async fn load_masp_params(
&self,
context_dir: JsValue,
- chain_id: &str,
+ chain_id: String,
) -> Result<(), JsValue> {
let context_dir = context_dir.as_string().unwrap();
let mut shielded = self.namada.shielded_mut().await;
- *shielded = ShieldedContext::new(masp::JSShieldedUtils::new(&context_dir, chain_id).await);
+ *shielded = ShieldedContext::new(masp::JSShieldedUtils::new(&context_dir, &chain_id).await);
Ok(())
}
@@ -233,18 +220,57 @@ impl Sdk {
}
}
- let signing_data = tx
- .signing_tx_data()?
- .iter()
- .cloned()
- .map(|std| (std, None))
- .collect::)>>();
+ to_js_result(borsh::to_vec(&namada_tx)?)
+ }
- // Recreate the tx with the new signatures, we can pass None for masp_signing_data as it
- // was already used
- let tx = tx::Tx::new(namada_tx, &borsh::to_vec(&tx.args())?, signing_data)?;
+ // TODO: this should be unified with sign_masp somehow
+ pub fn sign_masp_ledger(
+ &self,
+ tx: Vec,
+ signing_data: Box<[Uint8Array]>,
+ signature: Vec,
+ ) -> Result {
+ let mut namada_tx: Tx = borsh::from_slice(&tx)?;
+ let signing_data = signing_data
+ .iter()
+ .map(|sd| {
+ borsh::from_slice(&sd.to_vec()).expect("Expected to deserialize signing data")
+ })
+ .collect::>();
+
+ for signing_data in signing_data {
+ let signing_tx_data = signing_data.to_signing_tx_data()?;
+ if let Some(shielded_hash) = signing_tx_data.shielded_hash {
+ let mut masp_tx = namada_tx
+ .get_masp_section(&shielded_hash)
+ .expect("Expected to find the indicated MASP Transaction")
+ .clone();
+
+ let mut authorizations = HashMap::new();
+
+ let signature =
+ namada_sdk::masp_primitives::sapling::redjubjub::Signature::try_from_slice(
+ &signature.to_vec(),
+ )?;
+ // TODO: this works only if we assume that we do one
+ // shielded transfer in the transaction
+ authorizations.insert(0_usize, signature);
+
+ masp_tx = (*masp_tx)
+ .clone()
+ .map_authorization::(
+ (),
+ MapSaplingSigAuth(authorizations),
+ )
+ .freeze()
+ .unwrap();
+
+ namada_tx.remove_masp_section(&shielded_hash);
+ namada_tx.add_section(Section::MaspTx(masp_tx));
+ }
+ }
- to_js_result(borsh::to_vec(&tx)?)
+ to_js_result(borsh::to_vec(&namada_tx)?)
}
pub async fn sign_tx(
@@ -473,10 +499,14 @@ impl Sdk {
shielded_transfer_msg: &[u8],
wrapper_tx_msg: &[u8],
) -> Result {
- let mut args = args::shielded_transfer_tx_args(shielded_transfer_msg, wrapper_tx_msg)?;
- let bparams =
- generate_masp_build_params(MAX_HW_SPEND, MAX_HW_CONVERT, MAX_HW_OUTPUT, &args.tx)
- .await?;
+ let (mut args, bparams) =
+ args::shielded_transfer_tx_args(shielded_transfer_msg, wrapper_tx_msg)?;
+
+ let bparams = if let Some(bparams) = bparams {
+ BuildParams::StoredBuildParams(bparams)
+ } else {
+ generate_rng_build_params()
+ };
let _ = &self.namada.shielded_mut().await.load().await?;
@@ -514,11 +544,14 @@ impl Sdk {
unshielding_transfer_msg: &[u8],
wrapper_tx_msg: &[u8],
) -> Result {
- let mut args =
+ let (mut args, bparams) =
args::unshielding_transfer_tx_args(unshielding_transfer_msg, wrapper_tx_msg)?;
- let bparams =
- generate_masp_build_params(MAX_HW_SPEND, MAX_HW_CONVERT, MAX_HW_OUTPUT, &args.tx)
- .await?;
+
+ let bparams = if let Some(bparams) = bparams {
+ BuildParams::StoredBuildParams(bparams)
+ } else {
+ generate_rng_build_params()
+ };
let _ = &self.namada.shielded_mut().await.load().await?;
@@ -552,10 +585,13 @@ impl Sdk {
shielding_transfer_msg: &[u8],
wrapper_tx_msg: &[u8],
) -> Result {
- let mut args = args::shielding_transfer_tx_args(shielding_transfer_msg, wrapper_tx_msg)?;
- let bparams =
- generate_masp_build_params(MAX_HW_SPEND, MAX_HW_CONVERT, MAX_HW_OUTPUT, &args.tx)
- .await?;
+ let (mut args, bparams) =
+ args::shielding_transfer_tx_args(shielding_transfer_msg, wrapper_tx_msg)?;
+ let bparams = if let Some(bparams) = bparams {
+ BuildParams::StoredBuildParams(bparams)
+ } else {
+ generate_rng_build_params()
+ };
let _ = &self.namada.shielded_mut().await.load().await?;
let (tx, signing_data, _) = match bparams {
@@ -576,27 +612,11 @@ impl Sdk {
wrapper_tx_msg: &[u8],
) -> Result {
let args = args::ibc_transfer_tx_args(ibc_transfer_msg, wrapper_tx_msg)?;
- let bparams =
- generate_masp_build_params(MAX_HW_SPEND, MAX_HW_CONVERT, MAX_HW_OUTPUT, &args.tx)
- .await?;
-
- let ((tx, signing_data, _), bparams) = match bparams {
- BuildParams::RngBuildParams(mut bparams) => {
- let tx = build_ibc_transfer(&self.namada, &args, &mut bparams).await?;
- let bparams = bparams
- .to_stored()
- .ok_or_err_msg("Cannot convert bparams to stored")?;
-
- (tx, bparams)
- }
- BuildParams::StoredBuildParams(mut bparams) => {
- let tx = build_ibc_transfer(&self.namada, &args, &mut bparams).await?;
-
- (tx, bparams)
- }
- };
+ // TODO: we do not support ibc unshielding yet
+ let mut bparams = StoredBuildParams::default();
+ let (tx, signing_data, _) = build_ibc_transfer(&self.namada, &args, &mut bparams).await?;
- // As we can't get ExtendedFullViewingKeys from the tx args we need to get them from the
+ // As we can't get ExtendedFullViewingKeys from the tx args, we need to get them from the
// MASP Builder section of transaction
let masp_signing_data = if let Some(shielded_hash) = signing_data.shielded_hash {
let masp_builder = tx
diff --git a/packages/shared/lib/src/types/masp.rs b/packages/shared/lib/src/types/masp.rs
index cff5b901e8..e18de76e3f 100644
--- a/packages/shared/lib/src/types/masp.rs
+++ b/packages/shared/lib/src/types/masp.rs
@@ -47,6 +47,13 @@ impl ExtendedViewingKey {
pub fn encode(&self) -> String {
self.0.to_string()
}
+
+ pub fn default_payment_address(&self) -> PaymentAddress {
+ let xfvk = zip32::ExtendedFullViewingKey::from(self.0);
+ let (_, payment_address) = xfvk.default_address();
+
+ PaymentAddress(payment_address.into())
+ }
}
#[wasm_bindgen]
@@ -54,11 +61,20 @@ pub struct ProofGenerationKey(pub(crate) sapling::ProofGenerationKey);
#[wasm_bindgen]
impl ProofGenerationKey {
+ pub fn from_bytes(ak: Vec, nsk: Vec) -> ProofGenerationKey {
+ let concatenated: Vec = ak.iter().chain(nsk.iter()).cloned().collect();
+ let pgk = sapling::ProofGenerationKey::try_from_slice(concatenated.as_slice())
+ .expect("Deserializing ProofGenerationKey should not fail!");
+
+ ProofGenerationKey(pgk)
+ }
+
pub fn encode(&self) -> String {
hex::encode(
borsh::to_vec(&self.0).expect("Serializing ProofGenerationKey should not fail!"),
)
}
+
pub fn decode(encoded: String) -> ProofGenerationKey {
let decoded = hex::decode(encoded).expect("Decoding ProofGenerationKey should not fail!");
@@ -78,6 +94,7 @@ impl PseudoExtendedKey {
pub fn encode(&self) -> String {
hex::encode(borsh::to_vec(&self.0).expect("Serializing PseudoExtendedKey should not fail!"))
}
+
pub fn decode(encoded: String) -> PseudoExtendedKey {
let decoded = hex::decode(encoded).expect("Decoding PseudoExtendedKey should not fail!");
@@ -86,6 +103,18 @@ impl PseudoExtendedKey {
.expect("Deserializing ProofGenerationKey should not fail!"),
)
}
+
+ pub fn from(xvk: ExtendedViewingKey, pgk: ProofGenerationKey) -> Self {
+ let mut pxk = zip32::PseudoExtendedKey::from(zip32::ExtendedFullViewingKey::from(xvk.0));
+ pxk.augment_proof_generation_key(pgk.0)
+ .expect("Augmenting proof generation key should not fail!");
+
+ pxk.augment_spend_authorizing_key_unchecked(sapling::redjubjub::PrivateKey(
+ jubjub::Fr::default(),
+ ));
+
+ Self(pxk)
+ }
}
/// Wrap ExtendedSpendingKey
diff --git a/packages/types/src/tx/messages/index.ts b/packages/types/src/tx/messages/index.ts
index c98cf101fa..7f3c0a160e 100644
--- a/packages/types/src/tx/messages/index.ts
+++ b/packages/types/src/tx/messages/index.ts
@@ -10,6 +10,7 @@ export class Message implements IMessage {
try {
return serialize(value);
} catch (e) {
+ console.log("error", e);
throw new Error(`Unable to serialize message: ${e}`);
}
}
diff --git a/packages/types/src/tx/schema/transfer.ts b/packages/types/src/tx/schema/transfer.ts
index 8622cce513..e03aa0aaa4 100644
--- a/packages/types/src/tx/schema/transfer.ts
+++ b/packages/types/src/tx/schema/transfer.ts
@@ -47,6 +47,54 @@ export class TransparentTransferMsgValue {
}
}
+export class BparamsSpendMsgValue {
+ @field({ type: vec("u8") })
+ rcv!: Uint8Array;
+
+ @field({ type: vec("u8") })
+ alpha!: Uint8Array;
+
+ constructor(data: BparamsSpendMsgValue) {
+ Object.assign(this, data);
+ }
+}
+
+export class BparamsOutputMsgValue {
+ @field({ type: vec("u8") })
+ rcv!: Uint8Array;
+
+ @field({ type: vec("u8") })
+ rcm!: Uint8Array;
+
+ constructor(data: BparamsOutputMsgValue) {
+ Object.assign(this, data);
+ }
+}
+
+export class BparamsConvertMsgValue {
+ @field({ type: vec("u8") })
+ rcv!: Uint8Array;
+
+ constructor(data: BparamsConvertMsgValue) {
+ Object.assign(this, data);
+ }
+}
+
+export class BparamsMsgValue {
+ @field({ type: BparamsSpendMsgValue })
+ spend!: BparamsSpendMsgValue;
+
+ @field({ type: BparamsOutputMsgValue })
+ output!: BparamsOutputMsgValue;
+
+ @field({ type: BparamsConvertMsgValue })
+ convert!: BparamsConvertMsgValue;
+
+ constructor(data: BparamsMsgValue) {
+ Object.assign(this, data);
+ }
+}
+
/**
* Shielded Transfer schemas
*/
@@ -75,13 +123,24 @@ export class ShieldedTransferMsgValue {
@field({ type: option("string") })
gasSpendingKey?: string;
- constructor({ data, gasSpendingKey }: ShieldedTransferProps) {
+ @field({ type: option(vec(BparamsMsgValue)) })
+ bparams?: BparamsMsgValue[];
+
+ constructor({ data, gasSpendingKey, bparams }: ShieldedTransferProps) {
Object.assign(this, {
data: data.map(
(shieldedTransferDataProps) =>
new ShieldedTransferDataMsgValue(shieldedTransferDataProps)
),
gasSpendingKey,
+
+ bparams: bparams?.map((bparam) => {
+ return new BparamsMsgValue({
+ spend: new BparamsSpendMsgValue(bparam.spend),
+ output: new BparamsOutputMsgValue(bparam.output),
+ convert: new BparamsConvertMsgValue(bparam.convert),
+ });
+ }),
});
}
}
@@ -111,6 +170,9 @@ export class ShieldingTransferMsgValue {
@field({ type: vec(ShieldingTransferDataMsgValue) })
data!: ShieldingTransferDataMsgValue[];
+ @field({ type: option(vec(BparamsMsgValue)) })
+ bparams?: BparamsMsgValue[];
+
constructor({ data, target }: ShieldingTransferProps) {
Object.assign(this, {
target,
@@ -150,7 +212,15 @@ export class UnshieldingTransferMsgValue {
@field({ type: option("string") })
gasSpendingKey?: string;
- constructor({ source, data, gasSpendingKey }: UnshieldingTransferProps) {
+ @field({ type: option(vec(BparamsMsgValue)) })
+ bparams?: BparamsMsgValue[];
+
+ constructor({
+ source,
+ data,
+ gasSpendingKey,
+ bparams,
+ }: UnshieldingTransferProps) {
Object.assign(this, {
source,
data: data.map(
@@ -158,6 +228,13 @@ export class UnshieldingTransferMsgValue {
new UnshieldingTransferDataMsgValue(unshieldingTransferDataProps)
),
gasSpendingKey,
+ bparams: bparams?.map((bparam) => {
+ return new BparamsMsgValue({
+ spend: new BparamsSpendMsgValue(bparam.spend),
+ output: new BparamsOutputMsgValue(bparam.output),
+ convert: new BparamsConvertMsgValue(bparam.convert),
+ });
+ }),
});
}
}
diff --git a/yarn.lock b/yarn.lock
index b53d5f9ac4..abad424520 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3209,7 +3209,7 @@ __metadata:
languageName: node
linkType: hard
-"@ledgerhq/devices@npm:8.4.4, @ledgerhq/devices@npm:^8.4.4":
+"@ledgerhq/devices@npm:^8.4.4":
version: 8.4.4
resolution: "@ledgerhq/devices@npm:8.4.4"
dependencies:
@@ -3228,18 +3228,6 @@ __metadata:
languageName: node
linkType: hard
-"@ledgerhq/hw-transport-webhid@npm:^6.29.4":
- version: 6.30.0
- resolution: "@ledgerhq/hw-transport-webhid@npm:6.30.0"
- dependencies:
- "@ledgerhq/devices": "npm:8.4.4"
- "@ledgerhq/errors": "npm:^6.19.1"
- "@ledgerhq/hw-transport": "npm:^6.31.4"
- "@ledgerhq/logs": "npm:^6.12.0"
- checksum: 10c0/1cb6ddb50127d6cb73d80259e10da687a2b7aa87ebbac8cc3e770ac5b95a3ef0001bdaf77109da0eb62509cb8668a9642858b59cb0ff355c1adb0fe2114c532c
- languageName: node
- linkType: hard
-
"@ledgerhq/hw-transport-webusb@npm:^6.29.4":
version: 6.29.4
resolution: "@ledgerhq/hw-transport-webusb@npm:6.29.4"
@@ -3437,7 +3425,6 @@ __metadata:
"@cosmjs/encoding": "npm:^0.29.0"
"@dao-xyz/borsh": "npm:^5.1.5"
"@ledgerhq/hw-transport": "npm:^6.31.4"
- "@ledgerhq/hw-transport-webhid": "npm:^6.29.4"
"@ledgerhq/hw-transport-webusb": "npm:^6.29.4"
"@svgr/webpack": "npm:^6.3.1"
"@types/chrome": "npm:^0.0.237"
@@ -3450,7 +3437,7 @@ __metadata:
"@types/w3c-web-usb": "npm:^1.0.10"
"@types/webextension-polyfill": "npm:^0.10.6"
"@types/zxcvbn": "npm:^4.4.1"
- "@zondax/ledger-namada": "npm:^1.0.0"
+ "@zondax/ledger-namada": "npm:^2.0.0"
bignumber.js: "npm:^9.1.1"
buffer: "npm:^6.0.3"
copy-webpack-plugin: "npm:^11.0.0"
@@ -3716,11 +3703,10 @@ __metadata:
"@cosmjs/encoding": "npm:^0.29.0"
"@dao-xyz/borsh": "npm:^5.1.5"
"@ledgerhq/hw-transport": "npm:^6.31.4"
- "@ledgerhq/hw-transport-webhid": "npm:^6.29.4"
"@ledgerhq/hw-transport-webusb": "npm:^6.29.4"
"@types/jest": "npm:^29.5.12"
"@types/node": "npm:^20.11.4"
- "@zondax/ledger-namada": "npm:^1.0.0"
+ "@zondax/ledger-namada": "npm:^2.0.0"
babel-jest: "npm:^29.0.3"
bignumber.js: "npm:^9.1.1"
buffer: "npm:^6.0.3"
@@ -3736,6 +3722,7 @@ __metadata:
jest-mock-server: "npm:^0.1.0"
jsdoc-babel: "npm:^0.5.0"
rimraf: "npm:^5.0.5"
+ semver: "npm:^7.6.3"
slip44: "npm:^3.0.18"
ts-jest: "npm:^29.2.5"
ts-node: "npm:^10.9.1"
@@ -6090,12 +6077,12 @@ __metadata:
languageName: node
linkType: hard
-"@zondax/ledger-namada@npm:^1.0.0":
- version: 1.0.0
- resolution: "@zondax/ledger-namada@npm:1.0.0"
+"@zondax/ledger-namada@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "@zondax/ledger-namada@npm:2.0.0"
dependencies:
"@ledgerhq/hw-transport": "npm:^6.30.6"
- checksum: 10c0/f7490964ccd41f9a63f2bc8d89ea9f01e7d6a58be796db0b454a3f1455dcc8dc8dd578a8642aa0f0799c93f838e8e3afd69f59f7e9947816b9471338c2b9dd63
+ checksum: 10c0/1fa2a9a537bc42df01444332529a606ed77f608a2cc1dbb029915ed854ff447976930a4338c2d68d50d98869828cd76b1a4f4b5c2c989fd84af7b66d55dc51fc
languageName: node
linkType: hard