From 3e47963d9611f8e1bdf029ba3d3b249c207709cf Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Tue, 11 Feb 2025 09:30:48 -0500 Subject: [PATCH 1/9] feat: begin updating for new modified-zip32 --- .../src/background/keyring/keyring.ts | 23 ++++--------------- apps/extension/src/utils/index.ts | 15 ------------ packages/crypto/lib/src/crypto/zip32.rs | 6 ++--- packages/sdk/src/index.ts | 7 +++++- packages/sdk/src/keys/keys.ts | 22 ++++++++---------- packages/sdk/src/keys/types.ts | 18 +++++++++++++++ packages/types/src/account.ts | 1 + 7 files changed, 42 insertions(+), 50 deletions(-) diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index 54352f3233..2e0a0df973 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -1,4 +1,4 @@ -import { PhraseSize, ShieldedKeys } from "@namada/sdk/web"; +import { DEFAULT_ZIP32_PATH, PhraseSize, ShieldedKeys } from "@namada/sdk/web"; import { KVStore } from "@namada/storage"; import { AccountType, @@ -32,7 +32,7 @@ import { SensitiveType, VaultStorage, } from "storage"; -import { generateId, makeStoredPath } from "utils"; +import { generateId } from "utils"; // Generated UUID namespace for uuid v5 const UUID_NAMESPACE = "9bfceade-37fe-11ed-acc0-a3da3461b38c"; @@ -280,10 +280,7 @@ export class KeyRing { ): DerivedAccountInfo { // As this is derived from a parent account, our initial default account // should have a default path - const zip32Path = makeStoredPath( - AccountType.ShieldedKeys, - DEFAULT_BIP44_PATH - ); + const zip32Path = DEFAULT_ZIP32_PATH; const id = generateId( UUID_NAMESPACE, "shielded-account", @@ -299,11 +296,7 @@ export class KeyRing { const parentType = parentAccount.type; if (parentType === AccountType.Mnemonic) { - shieldedKeys = keysNs.deriveShieldedFromSeed( - secret, - bip44Path, - zip32Path - ); + shieldedKeys = keysNs.deriveShieldedFromSeed(secret, zip32Path); } else if (parentType === AccountType.PrivateKey) { shieldedKeys = keysNs.deriveShieldedFromPrivateKey(secret, zip32Path); } else { @@ -426,13 +419,7 @@ export class KeyRing { derivationPath = DEFAULT_BIP44_PATH; } - // We create a default zip32 path here as shielded keys will - // be derived from a private key that was derived with BIP44 - const zip32Path = makeStoredPath( - AccountType.ShieldedKeys, - DEFAULT_BIP44_PATH - ); - + const zip32Path = DEFAULT_ZIP32_PATH; const deriveFn = ( type === AccountType.PrivateKey ? this.deriveTransparentAccount diff --git a/apps/extension/src/utils/index.ts b/apps/extension/src/utils/index.ts index 12a5d5f208..e130369238 100644 --- a/apps/extension/src/utils/index.ts +++ b/apps/extension/src/utils/index.ts @@ -2,7 +2,6 @@ import { fromBase64, toBase64 } from "@cosmjs/encoding"; import { Account, AccountType, - Bip44Path, DerivedAccount, Path, TransferProps, @@ -107,20 +106,6 @@ export const fromEncodedTx = (encodedTxData: EncodedTxData): TxProps => ({ })), }); -// Generate either BIP44 or ZIP32 path from BIP44 based on account type -export const makeStoredPath = ( - accountType: AccountType, - path: Bip44Path -): Path => { - const { account, change, index } = path; - return accountType === AccountType.ShieldedKeys ? - // If this is a custom address (non-default account or index in the BIP44 path), - // specify index for shielded keys. In Namada CLI, the default ZIP32 path only - // specifies "account" - { account, index: account + index > 0 ? index : undefined } - : { account, change, index }; -}; - export const isCustomPath = (path: Path): boolean => { const { account, change = 0, index = 0 } = path; if (account + change + index > 0) { diff --git a/packages/crypto/lib/src/crypto/zip32.rs b/packages/crypto/lib/src/crypto/zip32.rs index d9645ec82d..8966800277 100644 --- a/packages/crypto/lib/src/crypto/zip32.rs +++ b/packages/crypto/lib/src/crypto/zip32.rs @@ -73,13 +73,13 @@ impl ShieldedHDWallet { // Optional address index let address_index = path.get(3); - let mut zip32_path: Vec = vec![purpose, coin_type, account] + let mut zip32_path: Vec = [purpose, coin_type, account] .iter() .map(|i| ChildIndex::Hardened(**i)) .collect(); - if address_index.is_some() { - zip32_path.push(ChildIndex::NonHardened(*address_index.unwrap())); + if let Some(index) = address_index { + zip32_path.push(ChildIndex::NonHardened(*index)); } let xsk: ExtendedSpendingKey = diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index cd7c35ece4..bb12c5dcab 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -35,7 +35,12 @@ export type { SupportedTx } from "./tx"; export { ProgressBarNames, Sdk, SdkEvents } from "./sdk"; -export { publicKeyToBech32 } from "./keys"; +export { + DEFAULT_BIP44_PATH, + DEFAULT_ZIP32_PATH, + MODIFIED_ZIP32_PATH, + publicKeyToBech32, +} from "./keys"; export type { Masp } from "./masp"; export { PhraseSize } from "./mnemonic"; diff --git a/packages/sdk/src/keys/keys.ts b/packages/sdk/src/keys/keys.ts index 5c79af6e93..fd8cc0a36b 100644 --- a/packages/sdk/src/keys/keys.ts +++ b/packages/sdk/src/keys/keys.ts @@ -15,17 +15,14 @@ import { } from "@namada/shared"; import { Bip44Path, Zip32Path } from "@namada/types"; import { makeBip44PathArray, makeSaplingPathArray } from "../utils"; -import { Address, ShieldedKeys, TransparentKeys } from "./types"; - -const DEFAULT_BIP44_PATH: Bip44Path = { - account: 0, - change: 0, - index: 0, -}; - -const DEFAULT_ZIP32_PATH: Zip32Path = { - account: 0, -}; +import { + Address, + DEFAULT_BIP44_PATH, + DEFAULT_ZIP32_PATH, + MODIFIED_ZIP32_PATH, + ShieldedKeys, + TransparentKeys, +} from "./types"; /** * Namespace for key related functions @@ -136,17 +133,16 @@ export class Keys { /** * Derive shielded keys and address from a seed and path * @param seed - Seed - * @param [bip44Path] - Bip44 path object to derive private key to seed the shielded keys * @param [zip32Path] - Zip32 path object to derive the shielded keys * @param [diversifier] - Diversifier bytes * @returns Shielded keys and address */ deriveShieldedFromSeed( seed: Uint8Array, - bip44Path: Bip44Path = DEFAULT_BIP44_PATH, zip32Path: Zip32Path = DEFAULT_ZIP32_PATH, diversifier?: Uint8Array ): ShieldedKeys { + const bip44Path: Bip44Path = MODIFIED_ZIP32_PATH; const shieldedHdWallet = new ShieldedHDWallet( seed, makeBip44PathArray(chains.namada.bip44.coinType, bip44Path) diff --git a/packages/sdk/src/keys/types.ts b/packages/sdk/src/keys/types.ts index de1c8ac2b5..0f1c99eae3 100644 --- a/packages/sdk/src/keys/types.ts +++ b/packages/sdk/src/keys/types.ts @@ -1,3 +1,5 @@ +import { Bip44Path, Zip32Path } from "@namada/types"; + /** * Address and public key type */ @@ -22,3 +24,19 @@ export type ShieldedKeys = { spendingKey: string; pseudoExtendedKey: string; }; + +export const DEFAULT_BIP44_PATH: Bip44Path = { + account: 0, + change: 0, + index: 0, +}; + +export const MODIFIED_ZIP32_PATH: Bip44Path = { + account: 0, + change: 0, + index: 2147483647, +}; + +export const DEFAULT_ZIP32_PATH: Zip32Path = { + account: 0, +}; diff --git a/packages/types/src/account.ts b/packages/types/src/account.ts index f5f5a9ee53..8407dff63d 100644 --- a/packages/types/src/account.ts +++ b/packages/types/src/account.ts @@ -37,6 +37,7 @@ export type DerivedAccount = { parentId?: string; path: Path; type: AccountType; + modifiedZip32Path?: string; pseudoExtendedKey?: string; source?: "imported" | "generated"; timestamp?: number; From c07fd8ec6c37513a0d221ed178f6409968118bfd Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Wed, 12 Feb 2025 06:01:49 -0500 Subject: [PATCH 2/9] fix: sdk tests --- packages/sdk/package.json | 1 + packages/sdk/src/tests/data.ts | 14 ++++++++------ packages/sdk/src/tests/keys.test.ts | 8 +++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 0156c679d0..13a916a38d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -52,6 +52,7 @@ "wasm:build:node:dev:multicore": "node ./scripts/build.cjs --node --multicore", "build:docs": "typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts", "test": "yarn wasm:build:node:dev && yarn jest", + "test:only": "yarn jest", "test:cov": "yarn wasm:build:node:dev && yarn jest --coverage", "test:watch": "yarn wasm:build:node:dev && yarn jest --watchAll=true", "test:ci": "jest" diff --git a/packages/sdk/src/tests/data.ts b/packages/sdk/src/tests/data.ts index 5972366cad..d8d195e185 100644 --- a/packages/sdk/src/tests/data.ts +++ b/packages/sdk/src/tests/data.ts @@ -26,20 +26,22 @@ export const ACCOUNT_2 = { export const SHIELDED_ACCOUNT_1 = { paymentAddress: - "znam1wzrkk8tcz8zxxv0j3ssckjnantguzj2gnzypvvmg0xwy6k45w4glvv2s9yhl8magw8dx7kz0u4h", + "znam1umjfq8d8s5n3chelg8xgv6cf5kh53cjkgclzuhnnp9nl548gmpjaajyuywla88xlpn0cuvcs86f", spendingKey: - "zsknam1qwhq8dchqqqqpq9kgajamqucsjt0gy8h9hdel8ga3ugynqs8lewj4ayqdnlk2reuf50h324g5rky8kqmy9u8v3306q03fa2ad32hdq8uacwrtzmn67gsz22vfw06r86vxyr9kk8ajr2n3ds5hsx0ks8fuuespr9y2v4ah9grz7snc33jhqgscvn7vwzpxplq5jwsmw5v6sew55er3y0s2mjw7q388w9xg8hdjfkd0n0hqdm32dl9mjvxkes5djmvq60u3dgka4jsyfgpxafgg", + "zsknam1qwwdw6q4qqqqpqy6qves7kzmv7lnul54ven0hw835smj5gu9zsac42gc7cagzlwauaf06exmuhw3dwxlfw5fdle3j6qdj9h5f5eu3r8nyr46jhsad5jqksresavcxffs5f7vhs8dflc70apu22kckyp24znapt2f0vpp4lgtfy98a35kd2rgydfrr25cuwf9xw9emfs6z3rvap87dprehwmwylz4glne2d3pqe8dds6h8ssczx8n6ps9zdeqe08fs3adm07uw5q83rqgtv7fq", viewingKey: - "zvknam1qwhq8dchqqqqpq9kgajamqucsjt0gy8h9hdel8ga3ugynqs8lewj4ayqdnlk2reufkyrsyfej5ez98ejnhuefnq6udfjadlvhwkqhkq0w447j088hkqqd6mg6k7wt363yslyqdx6cd6c8zavf4mwmwa6gn4wj0483dh6kzsaz7snc33jhqgscvn7vwzpxplq5jwsmw5v6sew55er3y0s2mjw7q388w9xg8hdjfkd0n0hqdm32dl9mjvxkes5djmvq60u3dgka4jsyfgeprxuv", + "zvknam1qwwdw6q4qqqqpqy6qves7kzmv7lnul54ven0hw835smj5gu9zsac42gc7cagzlwau7x9csywszmdj4fxhadd5464cxqt3p5kzqerkl8s7g4y48s4xs3t7gett2e8g32k2enh587ne7mwzmvetjtukpd9mwh8wlfckw4t5qyzfy98a35kd2rgydfrr25cuwf9xw9emfs6z3rvap87dprehwmwylz4glne2d3pqe8dds6h8ssczx8n6ps9zdeqe08fs3adm07uw5q83rq23y79r", + path: { account: 0 }, }; export const SHIELDED_ACCOUNT_2 = { paymentAddress: - "znam1rhgw9dmx2tm2dht8yaskftf90rgmz9vutadmvk2hsqsgg3uyze956qgjwl2rltvjrf9uwhp8ad6", + "znam1surgreuttyc0g3pjyh09hld5vfkmcxf5l6rfdhx4zl23sx9mg4p2venfdpevfj03yjyxzchyakk", spendingKey: - "zsknam1q0xzadt6qqqqpqr4h7htj8zwx52puekwckglcqf86yc0klku6vmntvjfqa7tnh5rsx3ntacx3mftkg5qc9fj3vjzj5v22y9k94hypuefy96qc798w9psn9a5jg00ljqv7udxj2u08yphnndr8y2fujrdagef7kfhgunv9zs803mlmuwuzdt4mygldzjxkkqqnj0al8x75093gxmynhy7egap5mxxttn3z3x05zt3qngd3sajmhaawn2qzwfjwtxa8hrnzw0mnnplf8cehs9kz", + "zsknam1qv4e0uyzqyqqpqyflphwkdus6je2er4vxdlmt4kd0eqt3afsp4r8ua7dc2c4gpau0x7km40uct3ve73a93mum75ldyvtq3g87ykh4rflpe0m8035qpzs9y996dmnc9t9x6a5mj0wl7xcdzfeczar3kttfhu9dl6m49gd7cgr9vka8cldpmkpvvlc4pvuzujg6r8q6nrzle2808mv0am6jg36lx9dat6z5pf7284dg7x8kgujuw5gvuaxfvj7q2h3eaj30rn3sek9trcssww6u", viewingKey: - "zvknam1q0xzadt6qqqqpqr4h7htj8zwx52puekwckglcqf86yc0klku6vmntvjfqa7tnh5rs9n3zp7f8dc7zutxdat068sd0m8qgs0fpm0dq4sjcr839p87d72mxwamvk4ev7tcdnq0xjzhw3hhes5vayem8ayuvvmr0y60llc59kqr03mlmuwuzdt4mygldzjxkkqqnj0al8x75093gxmynhy7egap5mxxttn3z3x05zt3qngd3sajmhaawn2qzwfjwtxa8hrnzw0mnnplf8ctxj0vu", + "zvknam1qv4e0uyzqyqqpqyflphwkdus6je2er4vxdlmt4kd0eqt3afsp4r8ua7dc2c4gpau0y9a8ac5cchspzm5w8cuepcxpdd0l0t5cttlx775tqlsh2ft9te60g4spv7fgzgrtk0kqtlk07vp3qj3qnvanu2jgeqyrgunc5y2uw2j9vka8cldpmkpvvlc4pvuzujg6r8q6nrzle2808mv0am6jg36lx9dat6z5pf7284dg7x8kgujuw5gvuaxfvj7q2h3eaj30rn3sek9trcuumrdf", + path: { account: 1 }, }; export const SIG_VALID = { diff --git a/packages/sdk/src/tests/keys.test.ts b/packages/sdk/src/tests/keys.test.ts index 1712ef49ba..27d48cf5b1 100644 --- a/packages/sdk/src/tests/keys.test.ts +++ b/packages/sdk/src/tests/keys.test.ts @@ -24,8 +24,10 @@ describe("Keys", () => { const seed = mnemonic.toSeed(mnemonic1); const seed2 = mnemonic.toSeed(mnemonic2); - const { address, viewingKey, spendingKey } = - keys.deriveShieldedFromSeed(seed); + const { address, viewingKey, spendingKey } = keys.deriveShieldedFromSeed( + seed, + shieldedAccount1.path + ); expect(address).toBe(shieldedAccount1.paymentAddress); expect(viewingKey).toBe(shieldedAccount1.viewingKey); @@ -35,7 +37,7 @@ describe("Keys", () => { address: address2, viewingKey: viewingKey2, spendingKey: spendingKey2, - } = keys.deriveShieldedFromSeed(seed2); + } = keys.deriveShieldedFromSeed(seed2, shieldedAccount2.path); expect(address2).toBe(shieldedAccount2.paymentAddress); expect(viewingKey2).toBe(shieldedAccount2.viewingKey); From 8eb3e7ce25e5cdb31abffe0ef37a9adbe8ef5ee8 Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Wed, 12 Feb 2025 06:40:01 -0500 Subject: [PATCH 3/9] feat: update stored value for indicating modified-zip32 derivation --- .../src/App/Accounts/ParentAccounts.tsx | 11 +++++++- .../src/background/keyring/keyring.ts | 27 ++++++++++++++++++- .../extension/src/background/keyring/types.ts | 1 + 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/extension/src/App/Accounts/ParentAccounts.tsx b/apps/extension/src/App/Accounts/ParentAccounts.tsx index b437d43548..25b0ef0ee3 100644 --- a/apps/extension/src/App/Accounts/ParentAccounts.tsx +++ b/apps/extension/src/App/Accounts/ParentAccounts.tsx @@ -33,7 +33,7 @@ export const ParentAccounts = (): JSX.Element => { (account) => account.parentId || account.type === AccountType.Ledger ) .map((account) => { - const outdated = + let outdated = account.type !== AccountType.Ledger && typeof account.pseudoExtendedKey === "undefined"; @@ -41,6 +41,15 @@ export const ParentAccounts = (): JSX.Element => { const parent = parentAccounts.find((pa) => pa.id === account.parentId) || account; + if (parent?.type === AccountType.Mnemonic) { + if ( + account.type === AccountType.ShieldedKeys && + !account.modifiedZip32Path + ) { + outdated = true; + } + } + return { ...parent, outdated }; }); diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index 2e0a0df973..f2050a542d 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -1,4 +1,11 @@ -import { DEFAULT_ZIP32_PATH, PhraseSize, ShieldedKeys } from "@namada/sdk/web"; +import { chains } from "@namada/chains"; +import { + DEFAULT_ZIP32_PATH, + makeBip44Path, + MODIFIED_ZIP32_PATH, + PhraseSize, + ShieldedKeys, +} from "@namada/sdk/web"; import { KVStore } from "@namada/storage"; import { AccountType, @@ -368,6 +375,23 @@ export class KeyRing { timestamp: number ): Promise { const { address, id, text, owner, pseudoExtendedKey } = derivedAccountInfo; + + let modifiedZip32Path: string | undefined; + + const parent = parentId ? await this.queryAccountById(parentId) : null; + + if (parent) { + if ( + type === AccountType.ShieldedKeys && + parent.type === AccountType.Mnemonic + ) { + modifiedZip32Path = makeBip44Path( + chains.namada.bip44.coinType, + MODIFIED_ZIP32_PATH + ); + } + } + const account: AccountStore = { id, address, @@ -375,6 +399,7 @@ export class KeyRing { parentId, path, type, + modifiedZip32Path, owner, pseudoExtendedKey, source, diff --git a/apps/extension/src/background/keyring/types.ts b/apps/extension/src/background/keyring/types.ts index 8f1f953db3..5c50c7d6a4 100644 --- a/apps/extension/src/background/keyring/types.ts +++ b/apps/extension/src/background/keyring/types.ts @@ -5,6 +5,7 @@ import { AccountType, DerivedAccount, Path } from "@namada/types"; export interface AccountStore extends StoredRecord { alias: string; address: string; + modifiedZip32Path?: string; owner: string; publicKey?: string; path: Path; From 1ff87804ebf587695731ffecb1ad4d22e1f81afa Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Wed, 12 Feb 2025 07:51:52 -0500 Subject: [PATCH 4/9] feat: begin implementing zip32 form, fix account persistence --- .../src/Setup/Common/AdvancedOptionsMenu.tsx | 20 +++-- apps/extension/src/Setup/Common/Zip32Form.tsx | 86 +++++++++++++++++++ .../Setup/ImportAccount/SeedPhraseImport.tsx | 20 +++-- .../src/Setup/Ledger/LedgerConnect.tsx | 22 +++-- .../src/Setup/Ledger/LedgerImport.tsx | 11 ++- apps/extension/src/Setup/Setup.tsx | 33 ++++--- .../src/storage/schemas/VaultSchemaV2.ts | 1 + 7 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 apps/extension/src/Setup/Common/Zip32Form.tsx diff --git a/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx b/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx index ee609e09e1..12c0da1ae8 100644 --- a/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx +++ b/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx @@ -1,16 +1,19 @@ import { Alert, LinkButton, Stack } from "@namada/components"; -import { Bip44Path } from "@namada/types"; +import { Bip44Path, Zip32Path } from "@namada/types"; import clsx from "clsx"; import React, { useState } from "react"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; import Bip39PassphraseForm from "./Bip39PassphraseForm"; import Bip44Form from "./Bip44Form"; +import Zip32Form from "./Zip32Form"; type Props = { passphrase: string; - path: Bip44Path; + bip44Path: Bip44Path; + zip32Path: Zip32Path; setPassphrase: (passphrase: string) => void; - setPath: (path: Bip44Path) => void; + setBip44Path: (path: Bip44Path) => void; + setZip32Path: (path: Zip32Path) => void; }; export enum Option { @@ -21,9 +24,11 @@ export enum Option { export const AdvancedOptionsMenu: React.FC = ({ passphrase, - path, + bip44Path, + zip32Path, setPassphrase, - setPath, + setBip44Path, + setZip32Path, }) => { const [option, setOption] = useState(Option.Menu); const menuText: Record = { @@ -84,7 +89,10 @@ export const AdvancedOptionsMenu: React.FC = ({ {option === Option.Path && ( - + <> + + + )} {option === Option.Passphrase && ( void; +}; + +const Zip32Form: React.FC = ({ path, setPath }) => { + const { coinType } = chains.namada.bip44; + + const handleNumericChange = ( + e: React.ChangeEvent + ): void => { + const result = e.target.value.replace(/\D/g, "") || "0"; + setPath({ + account: parseInt(result), + }); + }; + + const handleFocus = (e: React.ChangeEvent): void => + e.target.select(); + + const parentDerivationPath = `m/32'/${coinType}'/`; + + const handleReset = (): void => { + setPath({ account: 0 }); + }; + + return ( +
+
+ + +
    +
  • + You can create multiple addresses from one recovery phrase +
  • +
  • A lost path cannot be recovered
  • +
  • + If you're unfamiliar with this feature, skip or undo this + step -{" "} + { + e.preventDefault(); + handleReset(); + }} + > + Reset + +
  • +
+ +
+
+
+
+ ); +}; + +export default Zip32Form; diff --git a/apps/extension/src/Setup/ImportAccount/SeedPhraseImport.tsx b/apps/extension/src/Setup/ImportAccount/SeedPhraseImport.tsx index 10dc03a326..09c791fbfd 100644 --- a/apps/extension/src/Setup/ImportAccount/SeedPhraseImport.tsx +++ b/apps/extension/src/Setup/ImportAccount/SeedPhraseImport.tsx @@ -7,7 +7,7 @@ import { RadioGroup, Stack, } from "@namada/components"; -import { Bip44Path } from "@namada/types"; +import { Bip44Path, Zip32Path } from "@namada/types"; import { assertNever } from "@namada/utils"; import { AccountSecret, ValidateMnemonicMsg } from "background/keyring"; import { useRequester } from "hooks/useRequester"; @@ -19,8 +19,10 @@ import { fillArray, filterPrivateKeyPrefix, validatePrivateKey } from "utils"; type Props = { onConfirm: (accountSecret: AccountSecret) => void; - path: Bip44Path; - setPath: (path: Bip44Path) => void; + bip44Path: Bip44Path; + zip32Path: Zip32Path; + setBip44Path: (path: Bip44Path) => void; + setZip32Path: (path: Zip32Path) => void; }; const SHORT_PHRASE_COUNT = 12; @@ -34,8 +36,10 @@ enum SecretType { export const SeedPhraseImport: React.FC = ({ onConfirm, - path, - setPath, + bip44Path, + zip32Path, + setBip44Path, + setZip32Path, }) => { const requester = useRequester(); const [privateKey, setPrivateKey] = useState(""); @@ -241,8 +245,10 @@ export const SeedPhraseImport: React.FC = ({ {mnemonicType !== SecretType.PrivateKey && ( diff --git a/apps/extension/src/Setup/Ledger/LedgerConnect.tsx b/apps/extension/src/Setup/Ledger/LedgerConnect.tsx index 8dd0d176a4..5772f652cf 100644 --- a/apps/extension/src/Setup/Ledger/LedgerConnect.tsx +++ b/apps/extension/src/Setup/Ledger/LedgerConnect.tsx @@ -1,7 +1,7 @@ import { chains } from "@namada/chains"; import { ActionButton, Alert, Image, Stack } from "@namada/components"; import { Ledger as LedgerApp, makeBip44Path } from "@namada/sdk/web"; -import { Bip44Path } from "@namada/types"; +import { Bip44Path, Zip32Path } from "@namada/types"; import { LedgerError } from "@zondax/ledger-namada"; import { LedgerStep } from "Setup/Common"; import { AdvancedOptions } from "Setup/Common/AdvancedOptions"; @@ -11,11 +11,18 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; type Props = { - path: Bip44Path; - setPath: (path: Bip44Path) => void; + bip44Path: Bip44Path; + zip32Path: Zip32Path; + setBip44Path: (path: Bip44Path) => void; + setZip32Path: (path: Zip32Path) => void; }; -export const LedgerConnect: React.FC = ({ path, setPath }) => { +export const LedgerConnect: React.FC = ({ + bip44Path, + zip32Path: _zip32Path, // TODO + setBip44Path, + setZip32Path: _setZip32Path, // TODO +}) => { const navigate = useNavigate(); const [error, setError] = useState(); const [isLedgerConnecting, setIsLedgerConnecting] = useState(false); @@ -34,8 +41,9 @@ export const LedgerConnect: React.FC = ({ path, setPath }) => { setIsLedgerConnecting(true); const { address, publicKey } = await ledger.showAddressAndPublicKey( - makeBip44Path(chains.namada.bip44.coinType, path) + makeBip44Path(chains.namada.bip44.coinType, bip44Path) ); + // TODO: Shielded import will be enabled in PR: https://github.com/anoma/namada-interface/pull/1575 setIsLedgerConnecting(false); navigate(routes.ledgerImport(), { state: { @@ -99,7 +107,9 @@ export const LedgerConnect: React.FC = ({ path, setPath }) => { )} - + + {/* TODO: Enable in https://github.com/anoma/namada-interface/pull/1575 */} + {/**/} { const location = useLocation(); @@ -60,7 +62,8 @@ export const LedgerImport = ({ alias, address, publicKey, - path, + path: bip44Path, + // TODO: Enable shielded }); navigate(routes.ledgerComplete(), { diff --git a/apps/extension/src/Setup/Setup.tsx b/apps/extension/src/Setup/Setup.tsx index c08e1a3ea5..c21c92d617 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, DerivedAccount, Zip32Path } from "@namada/types"; import { assertNever } from "@namada/utils"; import { AccountSecret, AccountStore } from "background/keyring"; import { AnimatePresence, motion } from "framer-motion"; @@ -74,11 +74,14 @@ export const Setup: React.FC = () => { const [currentStep, setCurrentStep] = useState(0); const [totalSteps, setTotalSteps] = useState(0); const [currentPageTitle, setCurrentPageTitle] = useState(""); - const [path, setPath] = useState({ + const [bip44Path, setBip44Path] = useState({ account: 0, change: 0, index: 0, }); + const [zip32Path, setZip32Path] = useState({ + account: 0, + }); const [parentAccountStore, setParentAccountStore] = useState(); const [shieldedAccount, setShieldedAccount] = useState(); @@ -251,7 +254,7 @@ export const Setup: React.FC = () => { if (accountSecret) { void storeAccount({ ...accountCreationDetails, - path, + path: bip44Path, accountSecret, flow: "create", }); @@ -273,7 +276,8 @@ export const Setup: React.FC = () => { alias={accountCreationDetails.alias || ""} accountSecret={selectedAccountSecret} password={accountCreationDetails.password || ""} - path={path} + path={bip44Path} + // TODO: Display custom zip32 path across apps! parentAccountStore={parentAccountStore} shieldedAccount={shieldedAccount} status={completionStatus} @@ -298,8 +302,10 @@ export const Setup: React.FC = () => { element={ { setAccountSecret(accountSecret); navigate(routes.accountImportCreate()); @@ -328,7 +334,7 @@ export const Setup: React.FC = () => { if (accountSecret) { void storeAccount({ ...accountCreationDetails, - path, + path: bip44Path, accountSecret, flow: "import", }); @@ -350,7 +356,8 @@ export const Setup: React.FC = () => { alias={accountCreationDetails.alias || ""} accountSecret={selectedAccountSecret} password={accountCreationDetails.password || ""} - path={path} + path={bip44Path} + // TODO: Pass zip32 path! parentAccountStore={parentAccountStore} shieldedAccount={shieldedAccount} status={completionStatus} @@ -375,7 +382,12 @@ export const Setup: React.FC = () => { - + } /> @@ -389,7 +401,8 @@ export const Setup: React.FC = () => { )} > diff --git a/apps/extension/src/storage/schemas/VaultSchemaV2.ts b/apps/extension/src/storage/schemas/VaultSchemaV2.ts index 4dfde261ec..1322b0d174 100644 --- a/apps/extension/src/storage/schemas/VaultSchemaV2.ts +++ b/apps/extension/src/storage/schemas/VaultSchemaV2.ts @@ -70,6 +70,7 @@ export const KeyStore = t.exact( timestamp: t.number, }), t.partial({ + modifiedZip32Path: t.string, publicKey: t.string, parentId: t.string, pseudoExtendedKey: t.string, From 60dae6d034c8ac5d59129f999bc5e0d9b82a7c60 Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Thu, 13 Feb 2025 08:11:51 -0500 Subject: [PATCH 5/9] feat: hook up custom zip32 derivation --- .../src/Setup/Ledger/LedgerImport.tsx | 5 +- apps/extension/src/Setup/Setup.tsx | 6 +- apps/extension/src/Setup/query.ts | 22 ++--- apps/extension/src/Setup/types.ts | 8 +- .../src/background/keyring/handler.ts | 15 +-- apps/extension/src/background/keyring/init.ts | 4 +- .../src/background/keyring/keyring.ts | 96 +++++++------------ .../src/background/keyring/messages.ts | 24 ++--- .../src/background/keyring/service.ts | 7 +- 9 files changed, 83 insertions(+), 104 deletions(-) diff --git a/apps/extension/src/Setup/Ledger/LedgerImport.tsx b/apps/extension/src/Setup/Ledger/LedgerImport.tsx index a5bb800efb..aa67842614 100644 --- a/apps/extension/src/Setup/Ledger/LedgerImport.tsx +++ b/apps/extension/src/Setup/Ledger/LedgerImport.tsx @@ -20,7 +20,7 @@ type LedgerProps = { export const LedgerImport = ({ bip44Path, - zip32Path: _, + zip32Path, passwordRequired, }: LedgerProps): JSX.Element => { const location = useLocation(); @@ -62,7 +62,8 @@ export const LedgerImport = ({ alias, address, publicKey, - path: bip44Path, + bip44Path, + zip32Path, // TODO: Enable shielded }); diff --git a/apps/extension/src/Setup/Setup.tsx b/apps/extension/src/Setup/Setup.tsx index c21c92d617..234211067a 100644 --- a/apps/extension/src/Setup/Setup.tsx +++ b/apps/extension/src/Setup/Setup.tsx @@ -254,7 +254,8 @@ export const Setup: React.FC = () => { if (accountSecret) { void storeAccount({ ...accountCreationDetails, - path: bip44Path, + bip44Path, + zip32Path, accountSecret, flow: "create", }); @@ -334,7 +335,8 @@ export const Setup: React.FC = () => { if (accountSecret) { void storeAccount({ ...accountCreationDetails, - path: bip44Path, + bip44Path, + zip32Path, accountSecret, flow: "import", }); diff --git a/apps/extension/src/Setup/query.ts b/apps/extension/src/Setup/query.ts index ea8be11553..8ea64f083a 100644 --- a/apps/extension/src/Setup/query.ts +++ b/apps/extension/src/Setup/query.ts @@ -3,7 +3,7 @@ import { AccountStore, AddLedgerAccountMsg, DEFAULT_BIP44_PATH, - DeriveAccountMsg, + DeriveShieldedAccountMsg, SaveAccountSecretMsg, } from "background/keyring"; import { CreatePasswordMsg } from "background/vault"; @@ -38,9 +38,10 @@ export class AccountManager { async saveAccountSecret( details: DeriveAccountDetails ): Promise { - const { accountSecret, alias, path, flow } = details; + const { accountSecret, alias, bip44Path, flow } = details; + // On Private key imports, we don't know the derivation path, so just use default const derivationPath = - accountSecret?.t === "PrivateKey" ? DEFAULT_BIP44_PATH : path; + accountSecret?.t === "PrivateKey" ? DEFAULT_BIP44_PATH : bip44Path; const parentAccountStore = await this.requester.sendMessage( @@ -61,15 +62,13 @@ export class AccountManager { details: DeriveAccountDetails, parentAccount: AccountStore ): Promise { - const { path, accountSecret } = details; - const derivationPath = - accountSecret?.t === "PrivateKey" ? DEFAULT_BIP44_PATH : path; + const { zip32Path } = details; const { alias, id, source } = parentAccount; - return await this.requester.sendMessage( + return await this.requester.sendMessage( Ports.Background, - new DeriveAccountMsg( - derivationPath, + new DeriveShieldedAccountMsg( + zip32Path, AccountType.ShieldedKeys, alias, // Sets the parent ID of this shielded account @@ -88,10 +87,11 @@ export class AccountManager { async saveLedgerAccount( details: LedgerAccountDetails ): Promise { - const { alias, address, publicKey, path } = details; + // TODO: Support zip32 in upcoming PR! + const { alias, address, publicKey, bip44Path, zip32Path: _ } = details; return (await this.requester.sendMessage( Ports.Background, - new AddLedgerAccountMsg(alias, address, publicKey, path) + new AddLedgerAccountMsg(alias, address, publicKey, bip44Path) )) as AccountStore; } } diff --git a/apps/extension/src/Setup/types.ts b/apps/extension/src/Setup/types.ts index 4ea8253ed3..8c3c97f84a 100644 --- a/apps/extension/src/Setup/types.ts +++ b/apps/extension/src/Setup/types.ts @@ -1,4 +1,4 @@ -import { Bip44Path } from "@namada/types"; +import { Bip44Path, Zip32Path } from "@namada/types"; import { AccountSecret } from "background/keyring"; // Alias and optional password (in the case of Ledger accounts) @@ -10,13 +10,15 @@ export type AccountDetails = { export type DeriveAccountDetails = AccountDetails & { flow: "create" | "import"; accountSecret: AccountSecret; - path: Bip44Path; + bip44Path: Bip44Path; + zip32Path: Zip32Path; passwordRequired?: boolean; }; export type LedgerAccountDetails = { alias: string; - path: Bip44Path; + bip44Path: Bip44Path; + zip32Path: Zip32Path; address: string; publicKey: string; }; diff --git a/apps/extension/src/background/keyring/handler.ts b/apps/extension/src/background/keyring/handler.ts index 2d86d20d21..9c90db0842 100644 --- a/apps/extension/src/background/keyring/handler.ts +++ b/apps/extension/src/background/keyring/handler.ts @@ -9,7 +9,7 @@ import { Env, Handler, InternalHandler, Message } from "router"; import { AddLedgerAccountMsg, DeleteAccountMsg, - DeriveAccountMsg, + DeriveShieldedAccountMsg, GenerateMnemonicMsg, GetActiveAccountMsg, QueryAccountDetailsMsg, @@ -45,8 +45,11 @@ export const getHandler: (service: KeyRingService) => Handler = (service) => { env, msg as SaveAccountSecretMsg ); - case DeriveAccountMsg: - return handleDeriveAccountMsg(service)(env, msg as DeriveAccountMsg); + case DeriveShieldedAccountMsg: + return handleDeriveShieldedAccountMsg(service)( + env, + msg as DeriveShieldedAccountMsg + ); case QueryAccountsMsg: return handleQueryAccountsMsg(service)(env, msg as QueryAccountsMsg); case SetActiveAccountMsg: @@ -155,12 +158,12 @@ const handleRenameAccountMsg: ( }; }; -const handleDeriveAccountMsg: ( +const handleDeriveShieldedAccountMsg: ( service: KeyRingService -) => InternalHandler = (service) => { +) => InternalHandler = (service) => { return async (_, msg) => { const { path, accountType, alias, parentId, source } = msg; - return await service.deriveAccount( + return await service.deriveShieldedAccount( path, accountType, alias, diff --git a/apps/extension/src/background/keyring/init.ts b/apps/extension/src/background/keyring/init.ts index d3bea3a4c1..feb988980a 100644 --- a/apps/extension/src/background/keyring/init.ts +++ b/apps/extension/src/background/keyring/init.ts @@ -11,7 +11,7 @@ import { getHandler } from "./handler"; import { AddLedgerAccountMsg, DeleteAccountMsg, - DeriveAccountMsg, + DeriveShieldedAccountMsg, GenerateMnemonicMsg, GetActiveAccountMsg, QueryAccountDetailsMsg, @@ -25,7 +25,7 @@ import { import { KeyRingService } from "./service"; export function init(router: Router, service: KeyRingService): void { - router.registerMessage(DeriveAccountMsg); + router.registerMessage(DeriveShieldedAccountMsg); router.registerMessage(GenerateMnemonicMsg); router.registerMessage(GetActiveAccountMsg); router.registerMessage(QueryAccountsMsg); diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index f2050a542d..d55e19cc40 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -1,6 +1,5 @@ import { chains } from "@namada/chains"; import { - DEFAULT_ZIP32_PATH, makeBip44Path, MODIFIED_ZIP32_PATH, PhraseSize, @@ -15,6 +14,7 @@ import { Path, SignArbitraryResponse, TxProps, + Zip32Path, } from "@namada/types"; import { assertNever, Result, truncateInMiddle } from "@namada/utils"; @@ -280,48 +280,6 @@ export class KeyRing { }; } - public deriveShieldedAccount( - secret: Uint8Array, - bip44Path: Bip44Path, - parentAccount: DerivedAccount - ): DerivedAccountInfo { - // As this is derived from a parent account, our initial default account - // should have a default path - const zip32Path = DEFAULT_ZIP32_PATH; - const id = generateId( - UUID_NAMESPACE, - "shielded-account", - parentAccount.id, - // Specify unique identifiers for parent derived account - bip44Path.account, - bip44Path.change, - bip44Path.index - ); - const keysNs = this.sdkService.getSdk().getKeys(); - - let shieldedKeys: ShieldedKeys; - const parentType = parentAccount.type; - - if (parentType === AccountType.Mnemonic) { - shieldedKeys = keysNs.deriveShieldedFromSeed(secret, zip32Path); - } else if (parentType === AccountType.PrivateKey) { - shieldedKeys = keysNs.deriveShieldedFromPrivateKey(secret, zip32Path); - } else { - throw new Error(`Invalid account type! ${parentType}`); - } - - const { address, viewingKey, spendingKey, pseudoExtendedKey } = - shieldedKeys; - - return { - address, - id, - owner: viewingKey, - text: JSON.stringify({ spendingKey }), - pseudoExtendedKey, - }; - } - private async getParentSecret(parentId: string): Promise<{ secret: Uint8Array; }> { @@ -416,11 +374,8 @@ export class KeyRing { return account; } - /** - * Derive both shielded and transparent accounts from BIP44 Path - */ - public async deriveAccount( - bip44Path: Bip44Path, + public async deriveShieldedAccount( + path: Zip32Path, type: AccountType, alias: string, parentId: string, @@ -437,21 +392,36 @@ export class KeyRing { throw new Error(`Parent account not found: ${parentId}`); } - let derivationPath = bip44Path; - if (parentAccount.type === AccountType.PrivateKey) { - // Parent accounts that are imported private keys cannot - // contain custom paths, so ensure that we use the default here - derivationPath = DEFAULT_BIP44_PATH; + const { secret } = await this.getParentSecret(parentId); + const id = generateId( + UUID_NAMESPACE, + "shielded-account", + parentAccount.id, + path.account + ); + const keysNs = this.sdkService.getSdk().getKeys(); + + let shieldedKeys: ShieldedKeys; + const parentType = parentAccount.type; + + if (parentType === AccountType.Mnemonic) { + shieldedKeys = keysNs.deriveShieldedFromSeed(secret, path); + } else if (parentType === AccountType.PrivateKey) { + shieldedKeys = keysNs.deriveShieldedFromPrivateKey(secret, path); + } else { + throw new Error(`Invalid account type! ${parentType}`); } - const zip32Path = DEFAULT_ZIP32_PATH; - const deriveFn = ( - type === AccountType.PrivateKey ? - this.deriveTransparentAccount - : this.deriveShieldedAccount).bind(this); + const { address, viewingKey, spendingKey, pseudoExtendedKey } = + shieldedKeys; - const { secret } = await this.getParentSecret(parentId); - const info = deriveFn(secret, derivationPath, parentAccount); + const info = { + address, + id, + owner: viewingKey, + text: JSON.stringify({ spendingKey }), + pseudoExtendedKey, + }; // Check whether keys already exist for this account const existingAccount = await this.queryAccountByAddress(info.address); @@ -462,8 +432,8 @@ export class KeyRing { } const timestamp = source === "generated" ? new Date().getTime() : 0; - const derivedAccount = await this.persistAccount( - type === AccountType.ShieldedKeys ? zip32Path : derivationPath, + const derivedShieldedAccount = await this.persistAccount( + path, parentId, type, alias, @@ -471,7 +441,7 @@ export class KeyRing { source, timestamp ); - return derivedAccount; + return derivedShieldedAccount; } public async queryAllAccounts(): Promise { diff --git a/apps/extension/src/background/keyring/messages.ts b/apps/extension/src/background/keyring/messages.ts index 927657485d..2a268e5b39 100644 --- a/apps/extension/src/background/keyring/messages.ts +++ b/apps/extension/src/background/keyring/messages.ts @@ -1,5 +1,10 @@ import { PhraseSize } from "@namada/sdk/web"; -import { AccountType, Bip44Path, DerivedAccount } from "@namada/types"; +import { + AccountType, + Bip44Path, + DerivedAccount, + Zip32Path, +} from "@namada/types"; import { Result } from "@namada/utils"; import { ResponseSign } from "@zondax/ledger-namada"; import { Message } from "router"; @@ -14,7 +19,6 @@ import { } from "./types"; enum MessageType { - DeriveAccount = "derive-account", DeriveShieldedAccount = "derive-shielded-account", GenerateMnemonic = "generate-mnemonic", GetActiveAccount = "get-active-account", @@ -218,13 +222,13 @@ export class AddLedgerAccountMsg extends Message { } } -export class DeriveAccountMsg extends Message { +export class DeriveShieldedAccountMsg extends Message { public static type(): MessageType { - return MessageType.DeriveAccount; + return MessageType.DeriveShieldedAccount; } constructor( - public readonly path: Bip44Path, + public readonly path: Zip32Path, public readonly accountType: AccountType, public readonly alias: string, public readonly parentId: string, @@ -236,14 +240,10 @@ export class DeriveAccountMsg extends Message { validate(): void { validateProps(this, ["path", "accountType", "alias", "parentId", "source"]); - const { account, change } = this.path; + const { account } = this.path; if (!`${account}`) { - throw new Error("A Bip44Path account path was not provided!"); - } - - if (!`${change}`) { - throw new Error("A Bip44Path change path was not provided!"); + throw new Error("A Zip32Path account path was not provided!"); } } @@ -252,7 +252,7 @@ export class DeriveAccountMsg extends Message { } type(): string { - return DeriveAccountMsg.type(); + return DeriveShieldedAccountMsg.type(); } } diff --git a/apps/extension/src/background/keyring/service.ts b/apps/extension/src/background/keyring/service.ts index 4be1e92b53..ff4beb6bd4 100644 --- a/apps/extension/src/background/keyring/service.ts +++ b/apps/extension/src/background/keyring/service.ts @@ -7,6 +7,7 @@ import { GenDisposableSignerResponse, SignArbitraryResponse, TxProps, + Zip32Path, } from "@namada/types"; import { Result, truncateInMiddle } from "@namada/utils"; @@ -101,14 +102,14 @@ export class KeyRingService { return response; } - async deriveAccount( - path: Bip44Path, + async deriveShieldedAccount( + path: Zip32Path, type: AccountType, alias: string, parentId: string, source: "imported" | "generated" ): Promise { - const account = await this._keyRing.deriveAccount( + const account = await this._keyRing.deriveShieldedAccount( path, type, alias, From aa6b56eeba348c57691eb49cbc6bdf89b881c4f9 Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Thu, 13 Feb 2025 08:40:56 -0500 Subject: [PATCH 6/9] feat: clean up custom path components --- .../src/Setup/Common/AdvancedOptionsMenu.tsx | 11 +- apps/extension/src/Setup/Common/Bip44Form.tsx | 106 ++++++------------ .../src/Setup/Common/CustomPathForm.tsx | 64 +++++++++++ apps/extension/src/Setup/Common/Zip32Form.tsx | 70 +++--------- .../src/background/keyring/keyring.ts | 26 ----- 5 files changed, 125 insertions(+), 152 deletions(-) create mode 100644 apps/extension/src/Setup/Common/CustomPathForm.tsx diff --git a/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx b/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx index 12c0da1ae8..539928730e 100644 --- a/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx +++ b/apps/extension/src/Setup/Common/AdvancedOptionsMenu.tsx @@ -4,8 +4,7 @@ import clsx from "clsx"; import React, { useState } from "react"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; import Bip39PassphraseForm from "./Bip39PassphraseForm"; -import Bip44Form from "./Bip44Form"; -import Zip32Form from "./Zip32Form"; +import CustomPathForm from "./CustomPathForm"; type Props = { passphrase: string; @@ -90,8 +89,12 @@ export const AdvancedOptionsMenu: React.FC = ({ {option === Option.Path && ( <> - - + )} {option === Option.Passphrase && ( diff --git a/apps/extension/src/Setup/Common/Bip44Form.tsx b/apps/extension/src/Setup/Common/Bip44Form.tsx index 824a294dc3..63c291e679 100644 --- a/apps/extension/src/Setup/Common/Bip44Form.tsx +++ b/apps/extension/src/Setup/Common/Bip44Form.tsx @@ -1,7 +1,7 @@ import React from "react"; import { chains } from "@namada/chains"; -import { Alert, Input, LinkButton, Stack } from "@namada/components"; +import { Input } from "@namada/components"; import { Bip44Path } from "@namada/types"; type Props = { @@ -28,77 +28,45 @@ const Bip44Form: React.FC = ({ path, setPath }) => { const parentDerivationPath = `m/44'/${coinType}'/`; - const handleReset = (): void => { - setPath({ account: 0, change: 0, index: 0 }); - }; - return (
- - -
    -
  • - You can create multiple addresses from one recovery phrase -
  • -
  • A lost path cannot be recovered
  • -
  • - If you're unfamiliar with this feature, skip or undo this - step -{" "} - { - e.preventDefault(); - handleReset(); - }} - > - Reset - -
  • -
- -
-
+
); diff --git a/apps/extension/src/Setup/Common/CustomPathForm.tsx b/apps/extension/src/Setup/Common/CustomPathForm.tsx new file mode 100644 index 0000000000..5d02c7c0bb --- /dev/null +++ b/apps/extension/src/Setup/Common/CustomPathForm.tsx @@ -0,0 +1,64 @@ +import React from "react"; + +import { Alert, LinkButton, Stack } from "@namada/components"; +import { Bip44Path, Zip32Path } from "@namada/types"; +import Bip44Form from "./Bip44Form"; +import Zip32Form from "./Zip32Form"; + +type Props = { + bip44Path: Bip44Path; + zip32Path: Zip32Path; + setBip44Path: (path: Bip44Path) => void; + setZip32Path: (path: Zip32Path) => void; +}; + +const CustomPathForm: React.FC = ({ + bip44Path, + zip32Path, + setBip44Path, + setZip32Path, +}) => { + const handleReset = (): void => { + setBip44Path({ account: 0, change: 0, index: 0 }); + setZip32Path({ account: 0 }); + }; + + return ( +
+
+ + +
    +
  • + You can create multiple addresses from one recovery phrase +
  • +
  • A lost path cannot be recovered
  • +
  • + If you're unfamiliar with this feature, skip or undo this + step -{" "} + { + e.preventDefault(); + handleReset(); + }} + > + Reset + +
  • +
+ + +
+
+
+
+ ); +}; + +export default CustomPathForm; diff --git a/apps/extension/src/Setup/Common/Zip32Form.tsx b/apps/extension/src/Setup/Common/Zip32Form.tsx index 75eca8d26f..4f0b321a3c 100644 --- a/apps/extension/src/Setup/Common/Zip32Form.tsx +++ b/apps/extension/src/Setup/Common/Zip32Form.tsx @@ -1,7 +1,7 @@ import React from "react"; import { chains } from "@namada/chains"; -import { Alert, Input, LinkButton, Stack } from "@namada/components"; +import { Input } from "@namada/components"; import { Zip32Path } from "@namada/types"; type Props = { @@ -26,60 +26,24 @@ const Zip32Form: React.FC = ({ path, setPath }) => { const parentDerivationPath = `m/32'/${coinType}'/`; - const handleReset = (): void => { - setPath({ account: 0 }); - }; - return ( -
-
- - -
    -
  • - You can create multiple addresses from one recovery phrase -
  • -
  • A lost path cannot be recovered
  • -
  • - If you're unfamiliar with this feature, skip or undo this - step -{" "} - { - e.preventDefault(); - handleReset(); - }} - > - Reset - -
  • -
- -
-
+
+ ); }; diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index d55e19cc40..9be361e341 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -254,32 +254,6 @@ export class KeyRing { return accountStore; } - public deriveTransparentAccount( - seed: Uint8Array, - path: Bip44Path, - parentAccount: DerivedAccount - ): DerivedAccountInfo { - const keysNs = this.sdkService.getSdk().getKeys(); - const { address, privateKey } = keysNs.deriveFromSeed(seed, path); - - const { account, change, index } = path; - const id = generateId( - UUID_NAMESPACE, - "account", - parentAccount.id, - account, - change, - index - ); - - return { - address, - owner: address, - id, - text: privateKey, - }; - } - private async getParentSecret(parentId: string): Promise<{ secret: Uint8Array; }> { From de232824d49223106bda2d9a84b3dabf639c4a52 Mon Sep 17 00:00:00 2001 From: Justin Evans Date: Fri, 14 Feb 2025 07:25:28 -0500 Subject: [PATCH 7/9] fix: display parent account even if it doesn't have shielded --- .../src/App/Accounts/ParentAccounts.tsx | 51 ++++++++++++------- apps/extension/src/Setup/Common/Bip44Form.tsx | 2 +- apps/extension/src/Setup/Common/Zip32Form.tsx | 2 +- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/apps/extension/src/App/Accounts/ParentAccounts.tsx b/apps/extension/src/App/Accounts/ParentAccounts.tsx index 25b0ef0ee3..bf9d8ab92b 100644 --- a/apps/extension/src/App/Accounts/ParentAccounts.tsx +++ b/apps/extension/src/App/Accounts/ParentAccounts.tsx @@ -15,6 +15,7 @@ import { ParentAccount } from "background/keyring"; import { AccountContext } from "context"; import { openSetupTab } from "utils"; +type Account = DerivedAccount & { outdated: boolean }; /** * Represents the extension's settings page. */ @@ -27,34 +28,48 @@ export const ParentAccounts = (): JSX.Element => { changeActiveAccountId, } = useContext(AccountContext); + const allParentAccounts = parentAccounts.reduce( + (acc, account) => { + acc[account.id] = { ...account, outdated: false }; + return acc; + }, + {} as Record + ); + // We check which accounts need to be re-imported - const accounts = allAccounts + allAccounts .filter( (account) => account.parentId || account.type === AccountType.Ledger ) - .map((account) => { + .forEach((account) => { let 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; + if (account) { + const parent = allParentAccounts[account.parentId!]; + + if (parent) { + if (parent.type === AccountType.Mnemonic) { + if ( + account.type === AccountType.ShieldedKeys && + !account.modifiedZip32Path + ) { + outdated = true; + } + } - if (parent?.type === AccountType.Mnemonic) { - if ( - account.type === AccountType.ShieldedKeys && - !account.modifiedZip32Path - ) { - outdated = true; + allParentAccounts[parent.id] = { ...parent, outdated }; } } - - return { ...parent, outdated }; }); + const accounts = Object.values(allParentAccounts); + useEffect(() => { - const hasOutdatedAccounts = accounts.some((account) => account.outdated); + const hasOutdatedAccounts = accounts.some( + (account: Account) => account.outdated + ); // If there are outdated accounts, we redirect to the update required page if (hasOutdatedAccounts) { navigate(routes.accountsUpdateRequired()); @@ -65,19 +80,19 @@ export const ParentAccounts = (): JSX.Element => { await openSetupTab(); }; - const goToViewAccount = (account: DerivedAccount): void => { + const goToViewAccount = (account: Account): void => { navigate(routes.viewAccount(account.id)); }; - const goToDeletePage = (account: DerivedAccount): void => { + const goToDeletePage = (account: Account): void => { navigate(routes.deleteAccount(account.id), { state: { account } }); }; - const goToViewRecoveryPhrase = (account: DerivedAccount): void => { + const goToViewRecoveryPhrase = (account: Account): void => { navigate(routes.viewAccountMnemonic(account.id)); }; - const goToRenameAccount = (account: DerivedAccount): void => { + const goToRenameAccount = (account: Account): void => { navigate(routes.renameAccount(account.id), { state: { account } }); }; diff --git a/apps/extension/src/Setup/Common/Bip44Form.tsx b/apps/extension/src/Setup/Common/Bip44Form.tsx index 63c291e679..9c2252267f 100644 --- a/apps/extension/src/Setup/Common/Bip44Form.tsx +++ b/apps/extension/src/Setup/Common/Bip44Form.tsx @@ -32,7 +32,7 @@ const Bip44Form: React.FC = ({ path, setPath }) => {