From 1262caa59a22f7773acebb24caaab0f9abbba992 Mon Sep 17 00:00:00 2001 From: Tom Linton Date: Fri, 10 Mar 2023 16:36:50 +1300 Subject: [PATCH] allow mnemonic creation for accounts onboarded with ledger (#3224) * wip * force close parent and spacing tweak --- .../Onboarding/pages/OnboardAccount.tsx | 22 +-- .../Onboarding/pages/RecoverAccount.tsx | 45 +++--- .../Settings/AddConnectWallet/CreateMenu.tsx | 96 ++++++------ .../AddConnectWallet/CreateMnemonic.tsx | 140 ++++++++++++++++++ .../AddConnectWallet/ImportMnemonic.tsx | 24 +-- .../Settings/AddConnectWallet/index.tsx | 42 +++--- .../Settings/SettingsNavStackDrawer.tsx | 5 + .../common/Account/MnemonicInput.tsx | 80 +++++----- .../src/components/common/Layout/Drawer.tsx | 4 +- .../src/components/common/Layout/Router.tsx | 29 ++-- .../src/components/common/WalletList.tsx | 10 +- packages/background/src/backend/core.ts | 22 ++- .../background/src/backend/keyring/index.ts | 67 +++++++-- packages/background/src/frontend/server-ui.ts | 8 + .../blockchains/keyring/src/blockchain.ts | 26 +++- packages/common/src/constants.ts | 102 +++++++------ packages/recoil/src/atoms/keyring.tsx | 1 - packages/recoil/src/context/Notifications.tsx | 10 ++ 18 files changed, 479 insertions(+), 254 deletions(-) create mode 100644 packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx diff --git a/packages/app-extension/src/components/Onboarding/pages/OnboardAccount.tsx b/packages/app-extension/src/components/Onboarding/pages/OnboardAccount.tsx index 057205f30a..5fbab92b64 100644 --- a/packages/app-extension/src/components/Onboarding/pages/OnboardAccount.tsx +++ b/packages/app-extension/src/components/Onboarding/pages/OnboardAccount.tsx @@ -102,12 +102,12 @@ export const OnboardAccount = ({ key="MnemonicInput" readOnly={action === "create"} buttonLabel={action === "create" ? "Next" : "Import"} - onNext={(mnemonic) => { - setOnboardingData({ mnemonic }); - nextStep(); - }} - />, - ] + onNext={async (mnemonic) => { + setOnboardingData({ mnemonic }); + nextStep(); + }} + />, + ] : []), { - setOnboardingData({ password }); - nextStep(); - }} - />, - ] + setOnboardingData({ password }); + nextStep(); + }} + />, + ] : []), , , diff --git a/packages/app-extension/src/components/Onboarding/pages/RecoverAccount.tsx b/packages/app-extension/src/components/Onboarding/pages/RecoverAccount.tsx index 7ca5d7b161..baa4768872 100644 --- a/packages/app-extension/src/components/Onboarding/pages/RecoverAccount.tsx +++ b/packages/app-extension/src/components/Onboarding/pages/RecoverAccount.tsx @@ -87,43 +87,44 @@ export const RecoverAccount = ({ />, ...(keyringType === "mnemonic" ? [ - // Using a mnemonic + // Using a mnemonic { - setOnboardingData({ mnemonic }); - nextStep(); - }} - />, + onNext={async (mnemonic: string) => { + setOnboardingData({ mnemonic }); + nextStep(); + }} + />, ) => { - const signedWalletDescriptors = await Promise.all( - walletDescriptors.map(async (w) => ({ - ...w, - signature: await signMessageForWallet(w, authMessage), - })) - ); - setOnboardingData({ signedWalletDescriptors }); - nextStep(); - }} + const signedWalletDescriptors = await Promise.all( + walletDescriptors.map(async (w) => ({ + ...w, + signature: await signMessageForWallet(w, authMessage), + })) + ); + console.log(signedWalletDescriptors) + setOnboardingData({ signedWalletDescriptors }); + nextStep(); + }} onRetry={prevStep} - />, - ] + />, + ] : hardwareOnboardSteps), ...(!isAddingAccount ? [ { - setOnboardingData({ password }); - nextStep(); - }} - />, - ] + setOnboardingData({ password }); + nextStep(); + }} + />, + ] : []), ...(signedWalletDescriptors.length > 0 ? [] diff --git a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMenu.tsx b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMenu.tsx index e179dd9c1e..45cc06986d 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMenu.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMenu.tsx @@ -98,62 +98,60 @@ export function CreateMenu({ blockchain }: { blockchain: Blockchain }) { if (loading) { return; } - setOpenDrawer(true); - setLoading(true); - let newPublicKey; - if (!keyringExists || !hasHdPublicKeys) { - // No keyring or no existing mnemonic public keys so can't derive next - const walletDescriptor = await background.request({ - method: UI_RPC_METHOD_FIND_WALLET_DESCRIPTOR, - params: [blockchain, 0], - }); - const signature = await background.request({ - method: UI_RPC_METHOD_SIGN_MESSAGE_FOR_PUBLIC_KEY, - params: [ - blockchain, - walletDescriptor.publicKey, - base58.encode( - Buffer.from(getAddMessage(walletDescriptor.publicKey), "utf-8") - ), - [true, [walletDescriptor.derivationPath]], - ], - }); + if (hasMnemonic) { + setOpenDrawer(true); + setLoading(true); + let newPublicKey; + if (!keyringExists || !hasHdPublicKeys) { + // No keyring or no existing mnemonic public keys so can't derive next + const walletDescriptor = await background.request({ + method: UI_RPC_METHOD_FIND_WALLET_DESCRIPTOR, + params: [blockchain, 0], + }); + const signature = await background.request({ + method: UI_RPC_METHOD_SIGN_MESSAGE_FOR_PUBLIC_KEY, + params: [ + blockchain, + walletDescriptor.publicKey, + base58.encode( + Buffer.from(getAddMessage(walletDescriptor.publicKey), "utf-8") + ), + [true, [walletDescriptor.derivationPath]], + ], + }); + await background.request({ + method: UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, + params: [blockchain, { ...walletDescriptor, signature }], + }); + newPublicKey = walletDescriptor.publicKey; + // Keyring now exists, toggle to other options + setKeyringExists(true); + } else { + newPublicKey = await background.request({ + method: UI_RPC_METHOD_KEYRING_DERIVE_WALLET, + params: [blockchain], + }); + } await background.request({ - method: UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, - params: [{ ...walletDescriptor, signature }], + method: UI_RPC_METHOD_USER_ACCOUNT_READ, + params: [authenticatedUser?.jwt], }); - newPublicKey = walletDescriptor.publicKey; - // Keyring now exists, toggle to other options - setKeyringExists(true); + setNewPublicKey(newPublicKey); + setLoading(false); } else { - newPublicKey = await background.request({ - method: UI_RPC_METHOD_KEYRING_DERIVE_WALLET, - params: [blockchain], - }); + nav.push("create-mnemonic", { + blockchain, + keyringExists + }) } - - await background.request({ - method: UI_RPC_METHOD_USER_ACCOUNT_READ, - params: [authenticatedUser?.jwt], - }); - - setNewPublicKey(newPublicKey); - setLoading(false); }; const createMenu = { - ...(hasMnemonic - ? // TODO user should be guided through mnemonic creation flow if - // they don't have a mnemonic - // https://github.com/coral-xyz/backpack/issues/1464 - { - "Secret recovery phrase": { - onClick: createNewWithPhrase, - icon: (props: any) => , - detailIcon: , - }, - } - : {}), + "Secret recovery phrase": { + onClick: createNewWithPhrase, + icon: (props: any) => , + detailIcon: , + }, "Hardware wallet": { onClick: () => { openConnectHardware( diff --git a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx new file mode 100644 index 0000000000..0eba0e4ebe --- /dev/null +++ b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from "react"; +import type { + Blockchain, + SignedWalletDescriptor, +} from "@coral-xyz/common"; +import { + getAddMessage, + UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, + UI_RPC_METHOD_FIND_WALLET_DESCRIPTOR, + UI_RPC_METHOD_KEYRING_IMPORT_WALLET, + UI_RPC_METHOD_KEYRING_SET_MNEMONIC, + UI_RPC_METHOD_SIGN_MESSAGE_FOR_PUBLIC_KEY +} from "@coral-xyz/common"; +import { + useBackgroundClient, +} from "@coral-xyz/recoil"; +import { useCustomTheme } from "@coral-xyz/themes"; +import { ethers } from "ethers"; + +import { MnemonicInput } from "../../../common/Account/MnemonicInput"; +import { + useDrawerContext, + WithMiniDrawer, +} from "../../../common/Layout/Drawer"; +import { useNavigation } from "../../../common/Layout/NavStack"; + +const { base58 } = ethers.utils; + +import { ConfirmCreateWallet } from "./"; + +export function CreateMnemonic({ + blockchain, + keyringExists, +}: { + blockchain: Blockchain; + keyringExists: boolean; +}) { + const nav = useNavigation(); + const theme = useCustomTheme(); + const background = useBackgroundClient(); + const { close: closeParentDrawer } = useDrawerContext(); + + const [openDrawer, setOpenDrawer] = useState(false); + const [publicKey, setPublicKey] = useState(null); + + useEffect(() => { + const prevTitle = nav.title; + nav.setOptions({ headerTitle: "" }); + return () => { + nav.setOptions({ headerTitle: prevTitle }); + }; + }, [theme]); + + // TODO replace the left nav button to go to the previous step if step > 0 + + const onComplete = async (mnemonic: string, signedWalletDescriptor: SignedWalletDescriptor) => { + let publicKey: string; + await background.request({ + method: UI_RPC_METHOD_KEYRING_SET_MNEMONIC, + params: [mnemonic] + }) + if (keyringExists) { + // Using the keyring mnemonic and the blockchain keyring exists, just + // import the path + publicKey = await background.request({ + method: UI_RPC_METHOD_KEYRING_IMPORT_WALLET, + params: [blockchain, signedWalletDescriptor], + }); + } else { + // Blockchain keyring doesn't exist, init + publicKey = await background.request({ + method: UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, + params: [blockchain, signedWalletDescriptor], + }); + } + setPublicKey(publicKey); + setOpenDrawer(true); + }; + + return ( + <> + { + const walletDescriptor = await background.request({ + method: UI_RPC_METHOD_FIND_WALLET_DESCRIPTOR, + params: [blockchain, 0, mnemonic], + }); + + const signature = await background.request({ + method: UI_RPC_METHOD_SIGN_MESSAGE_FOR_PUBLIC_KEY, + params: [ + blockchain, + walletDescriptor.publicKey, + base58.encode( + Buffer.from( + getAddMessage(walletDescriptor.publicKey), + "utf-8" + ) + ), + [mnemonic, [walletDescriptor.derivationPath]], + ] + }) + + await onComplete(mnemonic, { + ...walletDescriptor, + signature + }) + }} + /> + { + // Must close parent when the confirm create wallet drawer closes because + // the next button in the mnemonic input screen is no longer valid as the users + // keyring has a mnemonic once it has been clicked once + if (!open) closeParentDrawer() + setOpenDrawer(open) + }} + backdropProps={{ + style: { + opacity: 0.8, + background: "#18181b", + }, + }} + > + { + setOpenDrawer(false); + closeParentDrawer(); + }} + /> + + + ); +} diff --git a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/ImportMnemonic.tsx b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/ImportMnemonic.tsx index 924c50734f..57badaa31d 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/ImportMnemonic.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/ImportMnemonic.tsx @@ -108,21 +108,21 @@ export function ImportMnemonic({ { - setMnemonic(mnemonic); - nextStep(); - }} - />, - // Must prompt for a name if using an input mnemonic, because we can't - // easily generate one + onNext={async (mnemonic) => { + setMnemonic(mnemonic); + nextStep(); + }} + />, + // Must prompt for a name if using an input mnemonic, because we can't + // easily generate one { - setName(name); - nextStep(); - }} - />, - ] + setName(name); + nextStep(); + }} + />, + ] : []), - nav.push("import-from-mnemonic", { - blockchain, - inputMnemonic: true, - keyringExists: true, - publicKey, - }), - icon: (props: any) => , - detailIcon: , - }, - "Private key": { - onClick: () => - nav.push("import-from-secret-key", { - blockchain, - publicKey, - }), - icon: (props: any) => , - detailIcon: , - }, - } + "Other recovery phrase": { + onClick: () => + nav.push("import-from-mnemonic", { + blockchain, + inputMnemonic: true, + keyringExists: true, + publicKey, + }), + icon: (props: any) => , + detailIcon: , + }, + "Private key": { + onClick: () => + nav.push("import-from-secret-key", { + blockchain, + publicKey, + }), + icon: (props: any) => , + detailIcon: , + }, + } : {}), }; diff --git a/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx b/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx index 992eeb7760..7b2808ec18 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx @@ -13,6 +13,7 @@ import { ContactRequests, Contacts } from "../Messages/Contacts"; import { Requests } from "../Messages/Requests"; import { CreateMenu } from "./AddConnectWallet/CreateMenu"; +import { CreateMnemonic } from "./AddConnectWallet/CreateMnemonic" import { ImportMenu } from "./AddConnectWallet/ImportMenu"; import { ImportMnemonic } from "./AddConnectWallet/ImportMnemonic"; import { ImportSecretKey } from "./AddConnectWallet/ImportSecretKey"; @@ -73,6 +74,10 @@ export function SettingsNavStackDrawer({ name="create-wallet" component={(props: any) => } /> + } + /> } diff --git a/packages/app-extension/src/components/common/Account/MnemonicInput.tsx b/packages/app-extension/src/components/common/Account/MnemonicInput.tsx index 51042f23f7..2ba188f6f6 100644 --- a/packages/app-extension/src/components/common/Account/MnemonicInput.tsx +++ b/packages/app-extension/src/components/common/Account/MnemonicInput.tsx @@ -68,11 +68,13 @@ export function MnemonicInput({ readOnly = false, buttonLabel, customError, + subtitle, }: { - onNext: (mnemonic: string) => void; + onNext: (mnemonic: string) => Promise; readOnly?: boolean; buttonLabel: string; customError?: string; + subtitle?: string; }) { const theme = useCustomTheme(); const classes = useStyles(); @@ -82,6 +84,7 @@ export function MnemonicInput({ ]); const [error, setError] = useState(); const [checked, setChecked] = useState(false); + const [loading, setLoading] = useState(false) const mnemonic = mnemonicWords.map((f) => f.trim()).join(" "); // Only enable copy all fields populated @@ -125,32 +128,29 @@ export function MnemonicInput({ // // Validate the mnemonic and call the onNext handler. // - const next = () => { - background + const next = async () => { + const isValid = await background .request({ method: UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC, params: [mnemonic], }) - .then((isValid: boolean) => { - return isValid - ? onNext(mnemonic) - : setError("Invalid secret recovery phrase"); - }); + if (!isValid) { + setError("Invalid secret recovery phrase"); + } else { + await onNext(mnemonic) + } }; // // Generate a random mnemonic and populate state. // - const generateRandom = () => { - background + const generateRandom = async () => { + const words = await background .request({ method: UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE, params: [mnemonicWords.length === 12 ? 128 : 256], }) - .then((m: string) => { - const words = m.split(" "); - setMnemonicWords(words); - }); + setMnemonicWords(words.split(" ")) }; return ( @@ -173,7 +173,7 @@ export function MnemonicInput({ }} /> - {readOnly + {subtitle ? subtitle : readOnly ? "This is the only way to recover your account if you lose your device. Write it down and store it in a safe place." : "Enter your 12 or 24-word secret recovery mnemonic to add an existing wallet."} @@ -189,21 +189,19 @@ export function MnemonicInput({ margin: "32px 0", }} > - <> - - - setMnemonicWords([ - ...Array(mnemonicWords.length === 12 ? 24 : 12).fill(""), - ]) - } - > - Use a {mnemonicWords.length === 12 ? "24" : "12"}-word - recovery mnemonic - - - + + + setMnemonicWords([ + ...Array(mnemonicWords.length === 12 ? 24 : 12).fill(""), + ]) + } + > + Use a {mnemonicWords.length === 12 ? "24" : "12"}-word + recovery mnemonic + + )} @@ -213,16 +211,16 @@ export function MnemonicInput({ icon={ - } + /> + } disabled={!copyEnabled} - /> - + /> + + /> : null} @@ -230,8 +228,12 @@ export function MnemonicInput({ { + setLoading(true) + await next() + setLoading(false) + }} + disabled={!nextEnabled || loading} buttonLabelStyle={{ fontWeight: 600, }} @@ -311,10 +313,10 @@ export function CopyButton({ style?: React.CSSProperties; }) { const [tooltipOpen, setTooltipOpen] = useState(false); - const onCopy = () => { + const onCopy = async () => { setTooltipOpen(true); setTimeout(() => setTooltipOpen(false), 1000); - navigator.clipboard.writeText(text); + await navigator.clipboard.writeText(text); }; return ( diff --git a/packages/app-extension/src/components/common/Layout/Drawer.tsx b/packages/app-extension/src/components/common/Layout/Drawer.tsx index f8981328bc..75820b4de5 100644 --- a/packages/app-extension/src/components/common/Layout/Drawer.tsx +++ b/packages/app-extension/src/components/common/Layout/Drawer.tsx @@ -5,10 +5,8 @@ import React, { type PropsWithChildren, type SetStateAction, useContext, - useEffect, } from "react"; import { EXTENSION_HEIGHT } from "@coral-xyz/common"; -import { useEphemeralNav } from "@coral-xyz/recoil"; import { styles } from "@coral-xyz/themes"; import { Close } from "@mui/icons-material"; import { Button, Drawer, IconButton } from "@mui/material"; @@ -203,7 +201,7 @@ export function WithDrawerNoHeader(props: any) { onClick={() => setOpenDrawer(false)} variant="contained" className={classes.closeDrawerButton} - > + > Close : null} diff --git a/packages/app-extension/src/components/common/Layout/Router.tsx b/packages/app-extension/src/components/common/Layout/Router.tsx index 03d54d302a..ddbb544cda 100644 --- a/packages/app-extension/src/components/common/Layout/Router.tsx +++ b/packages/app-extension/src/components/common/Layout/Router.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Navigate, Route, @@ -9,7 +9,6 @@ import { import type { SubscriptionType } from "@coral-xyz/common"; import { BACKPACK_TEAM, - MESSAGING_COMMUNICATION_FETCH_RESPONSE, NAV_COMPONENT_MESSAGE_PROFILE, } from "@coral-xyz/common"; import { @@ -419,16 +418,16 @@ function useNavBar() { {pathname.startsWith("/balances") ? "Balances" : pathname.startsWith("/apps") - ? "Applications" - : pathname.startsWith("/messages") && !isXs - ? "" - : pathname.startsWith("/messages") - ? "Messages" - : pathname.startsWith("/nfts") - ? "Collectibles" - : pathname.startsWith("/notifications") - ? "Notifications" - : "Recent Activity"} + ? "Applications" + : pathname.startsWith("/messages") && !isXs + ? "" + : pathname.startsWith("/messages") + ? "Messages" + : pathname.startsWith("/nfts") + ? "Collectibles" + : pathname.startsWith("/notifications") + ? "Notifications" + : "Recent Activity"} ); @@ -455,7 +454,7 @@ function useNavBar() { const notchViewComponent = pathname === "/nfts/chat" || pathname === "/messages/groupchat" ? ( - {}} /> + { }} /> ) : null; return { @@ -467,8 +466,8 @@ function useNavBar() { pathname === "/messages/chat" ? image : pathname === "/messages/groupchat" && props.id === "backpack-chat" - ? "https://user-images.githubusercontent.com/321395/206757416-a80e662a-0ccc-41cc-a20f-ff397755d47f.png" - : undefined, + ? "https://user-images.githubusercontent.com/321395/206757416-a80e662a-0ccc-41cc-a20f-ff397755d47f.png" + : undefined, isVerified: (pathname === "/messages/groupchat" && props.id === "backpack-chat") || (pathname === "/messages/chat" && BACKPACK_TEAM.includes(props.userId)), diff --git a/packages/app-extension/src/components/common/WalletList.tsx b/packages/app-extension/src/components/common/WalletList.tsx index cf6ddae8fc..44a4208c4c 100644 --- a/packages/app-extension/src/components/common/WalletList.tsx +++ b/packages/app-extension/src/components/common/WalletList.tsx @@ -12,7 +12,6 @@ import { SecretKeyIcon, } from "@coral-xyz/react-common"; import { - serverPublicKeys, useActiveWallet, useAllWallets, useBackgroundClient, @@ -22,13 +21,10 @@ import { } from "@coral-xyz/recoil"; import { styles, useCustomTheme } from "@coral-xyz/themes"; import { Add, ExpandMore, MoreHoriz } from "@mui/icons-material"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import DownloadIcon from "@mui/icons-material/Download"; import InfoIcon from "@mui/icons-material/Info"; import { Box, Button, Grid, Tooltip, Typography } from "@mui/material"; import type { SxProps, Theme } from "@mui/material/styles"; -import { useRecoilValue } from "recoil"; import { EthereumIconOnboarding as EthereumIcon, @@ -48,6 +44,7 @@ import { import { CreateMenu } from "../Unlocked/Settings/AddConnectWallet/CreateMenu"; import { ImportMenu } from "../Unlocked/Settings/AddConnectWallet/ImportMenu"; import { ImportMnemonic } from "../Unlocked/Settings/AddConnectWallet/ImportMnemonic"; +import { CreateMnemonic } from "../Unlocked/Settings/AddConnectWallet/CreateMnemonic"; import { ImportSecretKey } from "../Unlocked/Settings/AddConnectWallet/ImportSecretKey"; import { RemoveWallet } from "../Unlocked/Settings/YourAccount/EditWallets/RemoveWallet"; import { RenameWallet } from "../Unlocked/Settings/YourAccount/EditWallets/RenameWallet"; @@ -257,6 +254,11 @@ function WalletNavStack({ name="import-wallet" component={(props: any) => } /> + } + /> + } diff --git a/packages/background/src/backend/core.ts b/packages/background/src/backend/core.ts index 0efabd5ce4..45bc19340e 100644 --- a/packages/background/src/backend/core.ts +++ b/packages/background/src/backend/core.ts @@ -42,6 +42,7 @@ import { NOTIFICATION_KEYRING_IMPORTED_SECRET_KEY, NOTIFICATION_KEYRING_IMPORTED_WALLET, NOTIFICATION_KEYRING_KEY_DELETE, + NOTIFICATION_KEYRING_SET_MNEMONIC, NOTIFICATION_KEYRING_STORE_ACTIVE_USER_UPDATED, NOTIFICATION_KEYRING_STORE_CREATED, NOTIFICATION_KEYRING_STORE_LOCKED, @@ -205,11 +206,11 @@ export class Backend { const signersOrConf = "message" in tx ? ({ - accounts: { - encoding: "base64", - addresses, - }, - } as SimulateTransactionConfig) + accounts: { + encoding: "base64", + addresses, + }, + } as SimulateTransactionConfig) : undefined; return await this.solanaConnectionBackend.simulateTransaction( tx, @@ -461,7 +462,7 @@ export class Backend { return data.ethereum && data.ethereum.chainId ? data.ethereum.chainId : // Default to mainnet - "0x1"; + "0x1"; } async ethereumChainIdUpdate(chainId: string): Promise { @@ -1285,6 +1286,15 @@ export class Backend { return this.keyringStore.activeUserKeyring.hasMnemonic(); } + keyringSetMnemonic(mnemonic: string) { + this.keyringStore.activeUserKeyring.setMnemonic(mnemonic); + this.events.emit(BACKEND_EVENT, { + name: NOTIFICATION_KEYRING_SET_MNEMONIC, + }); + + + } + async previewPubkeys( blockchain: Blockchain, mnemonic: string, diff --git a/packages/background/src/backend/keyring/index.ts b/packages/background/src/backend/keyring/index.ts index c6d359ac92..2af3da0584 100644 --- a/packages/background/src/backend/keyring/index.ts +++ b/packages/background/src/backend/keyring/index.ts @@ -352,6 +352,25 @@ export class KeyringStore { }); } + /** + * Create a random mnemonic. + */ + public createMnemonic(strength: number): string { + return generateMnemonic(strength); + } + + public async activeUserUpdate(uuid: string): Promise { + const userData = await store.getUserData(); + const user = userData.users.filter((u) => u.uuid === uuid)[0]; + this.activeUserUuid = uuid; + await store.setActiveUser(user); + return user; + } + + /////////////////////////////////////////////////////////////////////////////// + // Locking methods methods + /////////////////////////////////////////////////////////////////////////////// + public async autoLockSettingsUpdate( seconds?: number, option?: AutolockSettingsOption @@ -371,20 +390,7 @@ export class KeyringStore { } public keepAlive() { - return this.withUnlock(() => {}); - } - - public createMnemonic(strength: number): string { - const mnemonic = generateMnemonic(strength); - return mnemonic; - } - - public async activeUserUpdate(uuid: string): Promise { - const userData = await store.getUserData(); - const user = userData.users.filter((u) => u.uuid === uuid)[0]; - this.activeUserUuid = uuid; - await store.setActiveUser(user); - return user; + return this.withUnlock(() => { }); } public autoLockCountdownToggle(enable: boolean) { @@ -543,6 +549,16 @@ export class KeyringStore { }); } + /** + * Set the mnemonic to be used by the hd keyring. + */ + public async setMnemonic(mnemonic: string) { + return await this.withUnlockAndPersist(async () => { + this.activeUserKeyring.setMnemonic(mnemonic) + }) + + } + /////////////////////////////////////////////////////////////////////////////// // Utilities. /////////////////////////////////////////////////////////////////////////////// @@ -808,13 +824,23 @@ class UserKeyring { } } - public addDerivationPath( + public async addDerivationPath( blockchain: Blockchain, derivationPath: string ): Promise<{ publicKey: string; name: string }> { let blockchainKeyring = this.blockchains.get(blockchain); if (!blockchainKeyring) { throw new Error("blockchain keyring not initialised"); + } else if (!blockchainKeyring.hasHdKeyring()) { + // Hd keyring not initialised, ibitialise it if possible + if (!this.mnemonic) { + throw new Error("hd keyring not initialised") + } + const accounts = await blockchainKeyring.initHdKeyring(this.mnemonic, [derivationPath]) + return { + publicKey: accounts[0][0], + name: accounts[0][1] + } } else { return blockchainKeyring.addDerivationPath(derivationPath); } @@ -840,10 +866,19 @@ class UserKeyring { } public exportMnemonic(): string { - if (!this.mnemonic) throw new Error("keyring does not have a mnemonic"); + if (!this.mnemonic) { + throw new Error("keyring does not have a mnemonic"); + } return this.mnemonic; } + public setMnemonic(mnemonic: string) { + if (this.mnemonic) { + throw new Error("keyring already has a mnemonic set"); + } + this.mnemonic = mnemonic + } + public async ledgerImport(walletDescriptor: WalletDescriptor) { const blockchainKeyring = this.blockchains.get(walletDescriptor.blockchain); const ledgerKeyring = blockchainKeyring!.ledgerKeyring!; diff --git a/packages/background/src/frontend/server-ui.ts b/packages/background/src/frontend/server-ui.ts index ca1b8520b9..717c607d49 100644 --- a/packages/background/src/frontend/server-ui.ts +++ b/packages/background/src/frontend/server-ui.ts @@ -50,6 +50,7 @@ import { UI_RPC_METHOD_KEYRING_EXPORT_MNEMONIC, UI_RPC_METHOD_KEYRING_EXPORT_SECRET_KEY, UI_RPC_METHOD_KEYRING_HAS_MNEMONIC, + UI_RPC_METHOD_KEYRING_SET_MNEMONIC, UI_RPC_METHOD_KEYRING_IMPORT_SECRET_KEY, UI_RPC_METHOD_KEYRING_IMPORT_WALLET, UI_RPC_METHOD_KEYRING_KEY_DELETE, @@ -202,6 +203,8 @@ async function handle( return handleKeyringExportSecretKey(ctx, params[0], params[1]); case UI_RPC_METHOD_KEYRING_HAS_MNEMONIC: return await handleKeyringHasMnemonic(ctx); + case UI_RPC_METHOD_KEYRING_SET_MNEMONIC: + return await handleKeyringSetMnemonic(ctx, params[0]); case UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC: return await handleValidateMnemonic(ctx, params[0]); case UI_RPC_METHOD_KEYRING_EXPORT_MNEMONIC: @@ -715,6 +718,11 @@ function handleKeyringHasMnemonic(ctx: Context): RpcResponse { return [resp]; } +function handleKeyringSetMnemonic(ctx: Context, mnemonic: string): RpcResponse { + const resp = ctx.backend.keyringSetMnemonic(mnemonic); + return [resp]; +} + function handleValidateMnemonic( ctx: Context, mnemonic: string diff --git a/packages/blockchains/keyring/src/blockchain.ts b/packages/blockchains/keyring/src/blockchain.ts index 97f265a04c..8eb8115446 100644 --- a/packages/blockchains/keyring/src/blockchain.ts +++ b/packages/blockchains/keyring/src/blockchain.ts @@ -62,18 +62,24 @@ export class BlockchainKeyring { mnemonic: string, derivationPaths: Array ): Promise> { - // Initialize keyrings. - this.hdKeyring = this.hdKeyringFactory.init(mnemonic, derivationPaths); // Empty ledger keyring to hold one off ledger imports this.ledgerKeyring = this.ledgerKeyringFactory.init([]); // Empty imported keyring to hold imported secret keys this.importedKeyring = this.keyringFactory.init([]); - this.activeWallet = this.hdKeyring.publicKeys()[0]; this.deletedWallets = []; + return this.initHdKeyring(mnemonic, derivationPaths); + } + public async initHdKeyring( + mnemonic: string, + derivationPaths: Array + ): Promise> { + // Initialize keyrings. + this.hdKeyring = this.hdKeyringFactory.init(mnemonic, derivationPaths); + this.activeWallet = this.hdKeyring!.publicKeys()[0]; // Persist a given name for this wallet. const newAccounts: Array<[string, string]> = []; - for (const [index, publicKey] of this.hdKeyring.publicKeys().entries()) { + for (const [index, publicKey] of this.hdKeyring!.publicKeys().entries()) { const name = DefaultKeyname.defaultDerived(index + 1); await store.setKeyname(publicKey, name); newAccounts.push([publicKey, name]); @@ -143,11 +149,15 @@ export class BlockchainKeyring { public async addDerivationPath( derivationPath: string ): Promise<{ publicKey: string; name: string }> { - const publicKey = this.hdKeyring!.addDerivationPath(derivationPath); + if (!this.hdKeyring) { + throw new Error("hd keyring not initialised") + } + + const publicKey = this.hdKeyring.addDerivationPath(derivationPath); // Save a default name. const name = DefaultKeyname.defaultDerived( - this.hdKeyring!.publicKeys().length + this.hdKeyring.publicKeys().length ); await store.setKeyname(publicKey, name); @@ -264,4 +274,8 @@ export class BlockchainKeyring { return false; } } + + public hasHdKeyring(): boolean { + return !!this.hdKeyring; + } } diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index df5caf4821..e071a9239c 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -124,6 +124,8 @@ export const UI_RPC_METHOD_KEYRING_READ_NEXT_DERIVATION_PATH = "ui-rpc-method-keyring-read-next-derivation-path"; export const UI_RPC_METHOD_KEYRING_IMPORT_WALLET = "ui-rpc-method-keyring-import-wallet"; +export const UI_RPC_METHOD_KEYRING_SET_MNEMONIC = + "ui-rpc-method-keyring-set-mnemonic"; export const UI_RPC_METHOD_KEYRING_DERIVE_WALLET = "ui-rpc-method-keyring-derive"; export const UI_RPC_METHOD_KEYRING_EXPORT_MNEMONIC = @@ -296,6 +298,8 @@ export const NOTIFICATION_KEYRING_IMPORTED_SECRET_KEY = "notification-keyring-imported-secret-key"; export const NOTIFICATION_KEYRING_KEY_DELETE = "notification-keyring-key-delete"; +export const NOTIFICATION_KEYRING_SET_MNEMONIC = + "notification-keyring-set-mnemonic"; export const NOTIFICATION_KEYRING_RESET_MNEMONIC = "notification-keyring-reset-mnemonic"; export const NOTIFICATION_KEYRING_STORE_CREATED = @@ -591,13 +595,13 @@ export const DEFAULT_GROUP_CHATS: { name: string; image: string; }[] = [ - { - id: "backpack-chat", - name: "Backpack", - image: - "https://user-images.githubusercontent.com/321395/206757416-a80e662a-0ccc-41cc-a20f-ff397755d47f.png", - }, -]; + { + id: "backpack-chat", + name: "Backpack", + image: + "https://user-images.githubusercontent.com/321395/206757416-a80e662a-0ccc-41cc-a20f-ff397755d47f.png", + }, + ]; export const WHITELISTED_CHAT_COLLECTIONS: { id: string; @@ -606,49 +610,49 @@ export const WHITELISTED_CHAT_COLLECTIONS: { collectionId: string; attributeMapping?: { [key: string]: string }; }[] = [ - { - id: "nouns", - name: "Y00ts + Nouns", - image: "https://metadata.y00ts.com/y/12189.png", - collectionId: "4mKSoDDqApmF1DqXvVTSL6tu2zixrSSNjqMxUnwvVzy2", - attributeMapping: { - Eyewear: "Nouns", + { + id: "nouns", + name: "Y00ts + Nouns", + image: "https://metadata.y00ts.com/y/12189.png", + collectionId: "4mKSoDDqApmF1DqXvVTSL6tu2zixrSSNjqMxUnwvVzy2", + attributeMapping: { + Eyewear: "Nouns", + }, }, - }, - { - id: "nokiamon", - name: "Nokiamon", - image: - "https://madlist-images.s3.us-west-2.amazonaws.com/nokiamon_pfp_1675332500467.png", - collectionId: "3YysdoK6ZcJFEL5QJxccY3q8AcTUFpahgbp4HFgBtjNF", - }, - { - id: "backpack-chat-internal", - name: "Backpack Team", - image: "https://one.xnfts.dev/BackpackTeamNFT.gif", - - collectionId: "BjN9u6zneFrjzuC7LH3eLaGC9FgYLnwQJMGA1xzVBKsj", - }, - { - id: "bonkz", - name: "BONKz", - image: - "https://bafybeiecuemcqxzuv4ti4sgffjlwvrqedr7golppwrbbu2u5yttglath3m.ipfs.nftstorage.link/0.png", - collectionId: "ajM4QBHtZBBRcMqqq9gawdHK28GXcb2yeRs6WBnqhay", - }, - { - id: "3PMczHyeW2ds7ZWDZbDSF3d21HBqG6yR4tG7vP6qczfj", - name: "The Madlist", - image: "https://www.madlads.com/mad_lads_logo.svg", - collectionId: "3PMczHyeW2ds7ZWDZbDSF3d21HBqG6yR4tG7vP6qczfj", - }, - { - id: "FCk24cq1pYhQo5MQYKHf5N9VnY8tdrToF7u6gvvsnGrn", - name: "The Madlist", - image: "https://www.madlads.com/mad_lads_logo.svg", - collectionId: "FCk24cq1pYhQo5MQYKHf5N9VnY8tdrToF7u6gvvsnGrn", - }, -]; + { + id: "nokiamon", + name: "Nokiamon", + image: + "https://madlist-images.s3.us-west-2.amazonaws.com/nokiamon_pfp_1675332500467.png", + collectionId: "3YysdoK6ZcJFEL5QJxccY3q8AcTUFpahgbp4HFgBtjNF", + }, + { + id: "backpack-chat-internal", + name: "Backpack Team", + image: "https://one.xnfts.dev/BackpackTeamNFT.gif", + + collectionId: "BjN9u6zneFrjzuC7LH3eLaGC9FgYLnwQJMGA1xzVBKsj", + }, + { + id: "bonkz", + name: "BONKz", + image: + "https://bafybeiecuemcqxzuv4ti4sgffjlwvrqedr7golppwrbbu2u5yttglath3m.ipfs.nftstorage.link/0.png", + collectionId: "ajM4QBHtZBBRcMqqq9gawdHK28GXcb2yeRs6WBnqhay", + }, + { + id: "3PMczHyeW2ds7ZWDZbDSF3d21HBqG6yR4tG7vP6qczfj", + name: "The Madlist", + image: "https://www.madlads.com/mad_lads_logo.svg", + collectionId: "3PMczHyeW2ds7ZWDZbDSF3d21HBqG6yR4tG7vP6qczfj", + }, + { + id: "FCk24cq1pYhQo5MQYKHf5N9VnY8tdrToF7u6gvvsnGrn", + name: "The Madlist", + image: "https://www.madlads.com/mad_lads_logo.svg", + collectionId: "FCk24cq1pYhQo5MQYKHf5N9VnY8tdrToF7u6gvvsnGrn", + }, + ]; // Load a fixed amount of public keys for various actions, e.g. import list, // searching mnemonics diff --git a/packages/recoil/src/atoms/keyring.tsx b/packages/recoil/src/atoms/keyring.tsx index 1110ae2764..7c03aa93c3 100644 --- a/packages/recoil/src/atoms/keyring.tsx +++ b/packages/recoil/src/atoms/keyring.tsx @@ -1,4 +1,3 @@ -import type { KeyringType } from "@coral-xyz/common"; import { UI_RPC_METHOD_KEYRING_HAS_MNEMONIC, UI_RPC_METHOD_KEYRING_STORE_STATE, diff --git a/packages/recoil/src/context/Notifications.tsx b/packages/recoil/src/context/Notifications.tsx index 76f36edf37..16dd7c2b5f 100644 --- a/packages/recoil/src/context/Notifications.tsx +++ b/packages/recoil/src/context/Notifications.tsx @@ -29,6 +29,7 @@ import { NOTIFICATION_KEYRING_IMPORTED_SECRET_KEY, NOTIFICATION_KEYRING_IMPORTED_WALLET, NOTIFICATION_KEYRING_KEY_DELETE, + NOTIFICATION_KEYRING_SET_MNEMONIC, NOTIFICATION_KEYRING_STORE_ACTIVE_USER_UPDATED, NOTIFICATION_KEYRING_STORE_CREATED, NOTIFICATION_KEYRING_STORE_LOCKED, @@ -95,6 +96,7 @@ export function NotificationsProvider(props: any) { }; }); }; + const setKeyringHasMnemonic = useSetRecoilState(atoms.keyringHasMnemonic) const setKeyringStoreState = useSetRecoilState(atoms.keyringStoreState); const setActiveUser = useSetRecoilState(atoms.user); const setAuthenticatedUser = useSetRecoilState(atoms.authenticatedUser); @@ -226,6 +228,9 @@ export function NotificationsProvider(props: any) { case NOTIFICATION_KEY_IS_COLD_UPDATE: handleKeyIsColdUpdate(notif); break; + case NOTIFICATION_KEYRING_SET_MNEMONIC: + handleKeyringSetMnemonic(); + break; case NOTIFICATION_KEYRING_STORE_CREATED: handleKeyringStoreCreated(notif); break; @@ -345,6 +350,11 @@ export function NotificationsProvider(props: any) { const handleKeyIsColdUpdate = (notif: Notification) => { setWalletData(notif.data.walletData); }; + + const handleKeyringSetMnemonic = () => { + setKeyringHasMnemonic(true) + } + const handleKeyringStoreCreated = (notif: Notification) => { setPreferences(notif.data.preferences); setKeyringStoreState(KeyringStoreStateEnum.Unlocked);