diff --git a/apps/extension/package.json b/apps/extension/package.json index 7ca1342e7b..a6d2b64ecc 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -40,9 +40,8 @@ "@cosmjs/encoding": "^0.29.0", "@dao-xyz/borsh": "^5.1.5", "@ledgerhq/hw-transport": "^6.31.4", - "@ledgerhq/hw-transport-webhid": "^6.29.4", "@ledgerhq/hw-transport-webusb": "^6.29.4", - "@zondax/ledger-namada": "^1.0.0", + "@zondax/ledger-namada": "^2.0.0", "bignumber.js": "^9.1.1", "buffer": "^6.0.3", "fp-ts": "^2.16.1", diff --git a/apps/extension/src/App/Accounts/ParentAccounts.tsx b/apps/extension/src/App/Accounts/ParentAccounts.tsx index b437d43548..f1b1251f99 100644 --- a/apps/extension/src/App/Accounts/ParentAccounts.tsx +++ b/apps/extension/src/App/Accounts/ParentAccounts.tsx @@ -1,3 +1,4 @@ +import invariant from "invariant"; import { useContext, useEffect } from "react"; import { Outlet, useNavigate } from "react-router-dom"; @@ -7,7 +8,7 @@ import { KeyListItem, Stack, } from "@namada/components"; -import { AccountType, DerivedAccount } from "@namada/types"; +import { DerivedAccount } from "@namada/types"; import { ParentAccountsFooter } from "App/Accounts/ParentAccountsFooter"; import { PageHeader } from "App/Common"; import routes from "App/routes"; @@ -26,20 +27,14 @@ export const ParentAccounts = (): JSX.Element => { accounts: allAccounts, changeActiveAccountId, } = useContext(AccountContext); - // We check which accounts need to be re-imported const accounts = allAccounts - .filter( - (account) => account.parentId || account.type === AccountType.Ledger - ) + .filter((account) => account.parentId) .map((account) => { - const outdated = - account.type !== AccountType.Ledger && - typeof account.pseudoExtendedKey === "undefined"; + const outdated = typeof account.pseudoExtendedKey === "undefined"; - // The only account without a parent is the ledger account - const parent = - parentAccounts.find((pa) => pa.id === account.parentId) || account; + const parent = parentAccounts.find((pa) => pa.id === account.parentId); + invariant(parent, `Parent account not found for account ${account.id}`); return { ...parent, outdated }; }); diff --git a/apps/extension/src/App/Accounts/UpdateRequired.tsx b/apps/extension/src/App/Accounts/UpdateRequired.tsx index d2dbad7dd1..7144207d61 100644 --- a/apps/extension/src/App/Accounts/UpdateRequired.tsx +++ b/apps/extension/src/App/Accounts/UpdateRequired.tsx @@ -75,11 +75,6 @@ export const UpdateRequired = (): JSX.Element => { -

- * Ledger accounts will receive shielded -
functions in a separate update in an -
upcoming release -

diff --git a/apps/extension/src/Approvals/ConfirmSignLedgerTx.tsx b/apps/extension/src/Approvals/ConfirmSignLedgerTx.tsx index ff7152d309..6f6a8e113b 100644 --- a/apps/extension/src/Approvals/ConfirmSignLedgerTx.tsx +++ b/apps/extension/src/Approvals/ConfirmSignLedgerTx.tsx @@ -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"; @@ -64,7 +72,7 @@ export const ConfirmSignLedgerTx: React.FC = ({ details }) => { useState(); const [isLedgerConnected, setIsLedgerConnected] = useState(false); const [ledger, setLedger] = useState(); - const { msgId, signer } = details; + const { msgId, signer, txDetails } = details; useEffect(() => { if (status === Status.Completed) { @@ -72,6 +80,32 @@ export const ConfirmSignLedgerTx: React.FC = ({ 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, @@ -90,6 +124,37 @@ export const ConfirmSignLedgerTx: React.FC = ({ 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 => { e.preventDefault(); @@ -122,6 +187,8 @@ export const ConfirmSignLedgerTx: React.FC = ({ 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) @@ -134,7 +201,7 @@ export const ConfirmSignLedgerTx: React.FC = ({ 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) @@ -146,8 +213,6 @@ export const ConfirmSignLedgerTx: React.FC = ({ details }) => { ); } - const signatures: ResponseSign[] = []; - let txIndex = 0; const txCount = pendingTxs.length; const stepTwoText = "Approve on your device"; @@ -156,6 +221,24 @@ export const ConfirmSignLedgerTx: React.FC = ({ details }) => { setStepTwoDescription(

{stepTwoText}

); } + // 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( @@ -166,20 +249,39 @@ export const ConfirmSignLedgerTx: React.FC = ({ details }) => {

); } - 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(

Submitting...

); - 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) { diff --git a/apps/extension/src/Approvals/ConfirmSignTx.tsx b/apps/extension/src/Approvals/ConfirmSignTx.tsx index d62b406406..3be85db9af 100644 --- a/apps/extension/src/Approvals/ConfirmSignTx.tsx +++ b/apps/extension/src/Approvals/ConfirmSignTx.tsx @@ -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"; @@ -41,6 +41,13 @@ export const ConfirmSignTx: React.FC = ({ 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) diff --git a/apps/extension/src/Setup/Common/Completion.tsx b/apps/extension/src/Setup/Common/Completion.tsx index 42bcca6619..58dbe6598b 100644 --- a/apps/extension/src/Setup/Common/Completion.tsx +++ b/apps/extension/src/Setup/Common/Completion.tsx @@ -4,7 +4,7 @@ import browser from "webextension-polyfill"; import { chains } from "@namada/chains"; import { ActionButton, Alert, Loading, ViewKeys } from "@namada/components"; import { makeBip44Path } from "@namada/sdk/web"; -import { Bip44Path, DerivedAccount } from "@namada/types"; +import { Bip44Path } from "@namada/types"; import { AccountSecret, AccountStore, @@ -20,7 +20,7 @@ type Props = { status?: CompletionStatus; statusInfo: string; parentAccountStore?: AccountStore; - shieldedAccount?: DerivedAccount; + paymentAddress?: string; password?: string; passwordRequired: boolean | undefined; path: Bip44Path; @@ -34,7 +34,7 @@ export const Completion: React.FC = (props) => { passwordRequired, path, parentAccountStore, - shieldedAccount, + paymentAddress, status, statusInfo, } = props; @@ -84,7 +84,7 @@ export const Completion: React.FC = (props) => { publicKeyAddress={parentAccountStore?.publicKey} transparentAccountAddress={parentAccountStore?.address} transparentAccountPath={transparentAccountPath} - shieldedAccountAddress={shieldedAccount?.address} + shieldedAccountAddress={paymentAddress} trimCharacters={35} footer={ { + const stepText = [ + "Deriving Bip44 public key...", + "Deriving Zip32 Viewing Key... This could take a few seconds!", + "Deriving Zip32 Proof-Generation Key... This could take a few seconds!", + ]; + + // Ensure that steps are within stepText limits + const totalSteps = stepText.length; + const currentStep = Math.min(Math.max(currentApprovalStep, 1), totalSteps); + + return ( + + + + + + + Approval {currentStep}/{totalSteps} + + + + Please wait for Ledger to respond! + +

+ {stepText[currentStep - 1]} +

+
+ ); +}; diff --git a/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx b/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx index a5a30b8d06..9f2fad04bf 100644 --- a/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx +++ b/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx @@ -13,7 +13,9 @@ export const LedgerConfirmation = (): JSX.Element => { return <>; } - const account = location.state.account as DerivedAccount; + const account = location.state.account as DerivedAccount & { + paymentAddress: string; + }; return (

@@ -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