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..25a1d130bd 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"; @@ -29,17 +30,14 @@ export const ParentAccounts = (): JSX.Element => { // 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"; - // 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/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/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..ef73b1178a 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; } 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..7c806f6f03 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(); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6b394d00b6..a637a6f4d9 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..29db7341ee 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 } 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..f39281bb42 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,8 @@ export type LedgerStatus = { info: ResponseAppInfo; }; +const LEDGER_MIN_VERSION_ZIP32 = "2.0.0"; + /** * Initialize USB transport * @async @@ -43,21 +41,16 @@ export const initLedgerUSBTransport = async (): Promise => { return await TransportUSB.create(); }; -/** - * Initialize HID transport - * @async - * @returns Transport object - */ -export const initLedgerHIDTransport = async (): Promise => { - return await TransportHID.create(); -}; - 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 */ @@ -147,20 +140,52 @@ 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. + * 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] Bip44 path for deriving key + * @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 +194,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 +250,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 + * @retuns 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/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/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/yarn.lock b/yarn.lock index bd8d2aa800..a64c1219e0 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" @@ -3430,7 +3418,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" @@ -3443,7 +3430,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" @@ -3707,11 +3694,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" @@ -3727,6 +3713,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" @@ -6072,12 +6059,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