diff --git a/packages/app-extension/src/components/Onboarding/pages/CreateOrImportWallet.tsx b/packages/app-extension/src/components/Onboarding/pages/CreateOrImportWallet.tsx index c932dba33..89c8c798e 100644 --- a/packages/app-extension/src/components/Onboarding/pages/CreateOrImportWallet.tsx +++ b/packages/app-extension/src/components/Onboarding/pages/CreateOrImportWallet.tsx @@ -1,4 +1,4 @@ -import { PrimaryButton } from "@coral-xyz/tamagui"; +import { PrimaryButton } from "@coral-xyz/react-common"; import { Box } from "@mui/material"; import { SubtextParagraph } from "../../common"; @@ -31,7 +31,7 @@ export const CreateOrImportWallet = ({ onNext("create")} + onClick={() => onNext("create")} /> onNext("import")}> diff --git a/packages/app-extension/src/components/Onboarding/pages/KeyringTypeSelector.tsx b/packages/app-extension/src/components/Onboarding/pages/KeyringTypeSelector.tsx index 0288635cf..7563e3fbb 100644 --- a/packages/app-extension/src/components/Onboarding/pages/KeyringTypeSelector.tsx +++ b/packages/app-extension/src/components/Onboarding/pages/KeyringTypeSelector.tsx @@ -1,6 +1,11 @@ +import { useState } from "react"; import type { KeyringType } from "@coral-xyz/common"; import { toTitleCase } from "@coral-xyz/common"; -import { HardwareWalletIcon, PrimaryButton } from "@coral-xyz/react-common"; +import { + HardwareWalletIcon, + PrimaryButton, + SecondaryButton, +} from "@coral-xyz/react-common"; import { Box } from "@mui/material"; import { Header, HeaderIcon, SubtextParagraph } from "../../common"; @@ -12,6 +17,7 @@ export const KeyringTypeSelector = ({ action: "create" | "import" | "recover" | string; onNext: (keyringType: KeyringType) => void; }) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); return (
onNext("mnemonic")} /> - {action === "import" || action === "recover" ? ( - - onNext("private-key")} - /> - - ) : null} - onNext("ledger")}> - {action === "recover" - ? "Recover using a hardware wallet" - : "I have a hardware wallet"} - + {showAdvancedOptions ? ( + <> + {action === "import" || action === "recover" ? ( + + onNext("private-key")} + /> + + ) : null} + + onNext("ledger")} + /> + + setShowAdvancedOptions(false)}> + Hide advanced options + + + ) : ( + setShowAdvancedOptions(true)}> + Show advanced options + + )}
); diff --git a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx index 285196853..6a0a99b57 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/CreateMnemonic.tsx @@ -47,11 +47,9 @@ export function CreateOrImportMnemonic({ }, "Import recovery phrase": { onClick: () => - nav.push("import-from-mnemonic", { + nav.push("set-and-sync-mnemonic", { blockchain, keyringExists, - forceSetMnemonic: true, - inputMnemonic: true, }), icon: (props: any) => , detailIcon: , 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 910fe764c..0eb189413 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/ImportMnemonic.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/ImportMnemonic.tsx @@ -11,11 +11,16 @@ import { UI_RPC_METHOD_KEYRING_IMPORT_SECRET_KEY, UI_RPC_METHOD_KEYRING_IMPORT_WALLET, UI_RPC_METHOD_KEYRING_SET_MNEMONIC, + UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_SYNC, } from "@coral-xyz/common"; -import { PrimaryButton, TextInput } from "@coral-xyz/react-common"; -import { useBackgroundClient, useRpcRequests } from "@coral-xyz/recoil"; +import { CheckIcon, PrimaryButton, TextInput } from "@coral-xyz/react-common"; +import { + useBackgroundClient, + useDehydratedWallets, + useRpcRequests, +} from "@coral-xyz/recoil"; import { useCustomTheme } from "@coral-xyz/themes"; -import { Box } from "@mui/material"; +import { Box, Typography } from "@mui/material"; import { useSteps } from "../../../../hooks/useSteps"; import { Header } from "../../../common"; @@ -29,6 +34,103 @@ import { useNavigation } from "../../../common/Layout/NavStack"; import { ConfirmCreateWallet } from "./"; +// WARNING: this will force set the mnemonic. Only use this if no mnemonic +// exists. +export function ImportMnemonicAutomatic() { + const background = useBackgroundClient(); + const dehydratedWallets = useDehydratedWallets(); + const [openDrawer, setOpenDrawer] = useState(false); + const { close } = useDrawerContext(); + + const onSync = async (mnemonic: string) => { + await background.request({ + method: UI_RPC_METHOD_KEYRING_SET_MNEMONIC, + params: [mnemonic], + }); + if (dehydratedWallets.length > 0) { + await background.request({ + method: UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_SYNC, + params: [dehydratedWallets], + }); + } + }; + + return ( + <> + { + onSync(mnemonic); + setOpenDrawer(true); + }} + /> + { + setOpenDrawer(isOpen); + if (!isOpen) { + close(); + } + }} + backdropProps={{ + style: { + opacity: 0.8, + background: "#18181b", + }, + }} + > + { + setOpenDrawer(false); + close(); + }} + /> + + + ); +} + +export const ConfirmWalletSync = ({ onClose }: { onClose: () => void }) => { + const theme = useCustomTheme(); + return ( +
+
+ + Recovery Phrase Set + +
+ +
+
+ onClose()} /> +
+ ); +}; + export function ImportMnemonic({ blockchain, keyringExists, @@ -48,6 +150,7 @@ export function ImportMnemonic({ const { step, nextStep } = useSteps(); const { close: closeParentDrawer } = useDrawerContext(); const { signMessageForWallet } = useRpcRequests(); + const dehydratedWallets = useDehydratedWallets(); const [openDrawer, setOpenDrawer] = useState(false); const [mnemonic, setMnemonic] = useState(true); @@ -78,6 +181,15 @@ export function ImportMnemonic({ method: UI_RPC_METHOD_KEYRING_IMPORT_WALLET, params: [signedWalletDescriptor], }); + const walletsToSync = dehydratedWallets.filter( + (w) => w.publicKey !== publicKey + ); + if (walletsToSync.length > 0) { + await background.request({ + method: UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_SYNC, + params: [walletsToSync], + }); + } } else { if (!inputMnemonic) { if (keyringExists) { diff --git a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/index.tsx b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/index.tsx index 3f3059bbb..6bba0cad1 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/index.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/AddConnectWallet/index.tsx @@ -10,6 +10,7 @@ import { UI_RPC_METHOD_KEYRING_IMPORT_WALLET, } from "@coral-xyz/common"; import { + BackpackMnemonicIcon, CheckIcon, HardwareIcon, ImportedIcon, @@ -24,6 +25,7 @@ import { import { useAvatarUrl, useBackgroundClient, + useEnabledBlockchains, useKeyringHasMnemonic, useRpcRequests, useUser, @@ -191,64 +193,58 @@ export function AddWalletMenu({ blockchain }: { blockchain: Blockchain }) { if (loading) { return; } - 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 signMessageForWallet( - blockchain, - walletDescriptor.publicKey, - getAddMessage(walletDescriptor.publicKey), - { - mnemonic: true, - signedWalletDescriptors: [ - { - ...walletDescriptor, - signature: "", - }, - ], - } - ); - const signedWalletDescriptor = { ...walletDescriptor, signature }; - if (!keyringExists) { - // Keyring doesn't exist, create it - await background.request({ - method: UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, - params: [ - { - mnemonic: true, // Use the existing mnemonic - signedWalletDescriptors: [signedWalletDescriptor], - }, - ], - }); - } else { - // Keyring exists but the hd keyring is not initialised, import - await background.request({ - method: UI_RPC_METHOD_KEYRING_IMPORT_WALLET, - params: [signedWalletDescriptor], - }); + + 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 signMessageForWallet( + blockchain, + walletDescriptor.publicKey, + getAddMessage(walletDescriptor.publicKey), + { + mnemonic: true, + signedWalletDescriptors: [ + { + ...walletDescriptor, + signature: "", + }, + ], } - newPublicKey = walletDescriptor.publicKey; + ); + const signedWalletDescriptor = { ...walletDescriptor, signature }; + if (!keyringExists) { + // Keyring doesn't exist, create it + await background.request({ + method: UI_RPC_METHOD_BLOCKCHAIN_KEYRINGS_ADD, + params: [ + { + mnemonic: true, // Use the existing mnemonic + signedWalletDescriptors: [signedWalletDescriptor], + }, + ], + }); } else { - newPublicKey = await background.request({ - method: UI_RPC_METHOD_KEYRING_DERIVE_WALLET, - params: [blockchain], + // Keyring exists but the hd keyring is not initialised, import + await background.request({ + method: UI_RPC_METHOD_KEYRING_IMPORT_WALLET, + params: [signedWalletDescriptor], }); } - setNewPublicKey(newPublicKey); - setLoading(false); + newPublicKey = walletDescriptor.publicKey; } else { - nav.push("create-or-import-mnemonic", { - blockchain, - keyringExists, + newPublicKey = await background.request({ + method: UI_RPC_METHOD_KEYRING_DERIVE_WALLET, + params: [blockchain], }); } + setNewPublicKey(newPublicKey); + setLoading(false); }; return ( @@ -268,8 +264,14 @@ export function AddWalletMenu({ blockchain }: { blockchain: Blockchain }) { createNewWithPhrase(), + [hasMnemonic ? "Create a new wallet" : "Setup recovery phrase"]: { + onClick: () => + hasMnemonic + ? createNewWithPhrase() + : nav.push("create-or-import-mnemonic", { + blockchain, + keyringExists, + }), icon: (props: any) => , }, "Advanced wallet import": { @@ -316,22 +318,16 @@ export function RecoverWalletMenu({ publicKey: string; }) { const nav = useNavigation(); + const enabledBlockchains = useEnabledBlockchains(); + const keyringExists = enabledBlockchains.includes(blockchain); const recoverMenu = { - "Hardware wallet": { - onClick: () => { - openConnectHardware(blockchain, "search", publicKey); - window.close(); - }, - icon: (props: any) => , - detailIcon: , - }, "Other recovery phrase": { onClick: () => nav.push("import-from-mnemonic", { blockchain, inputMnemonic: true, - keyringExists: true, + keyringExists, publicKey, }), icon: (props: any) => , @@ -346,6 +342,14 @@ export function RecoverWalletMenu({ icon: (props: any) => , detailIcon: , }, + "Hardware wallet": { + onClick: () => { + openConnectHardware(blockchain, "search", publicKey); + window.close(); + }, + icon: (props: any) => , + detailIcon: , + }, }; return ( diff --git a/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx b/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx index 140c78592..931e61956 100644 --- a/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx +++ b/packages/app-extension/src/components/Unlocked/Settings/SettingsNavStackDrawer.tsx @@ -17,7 +17,10 @@ import { CreateOrImportMnemonic, } from "./AddConnectWallet/CreateMnemonic"; import { ImportMenu } from "./AddConnectWallet/ImportMenu"; -import { ImportMnemonic } from "./AddConnectWallet/ImportMnemonic"; +import { + ImportMnemonic, + ImportMnemonicAutomatic, +} from "./AddConnectWallet/ImportMnemonic"; import { ImportSecretKey } from "./AddConnectWallet/ImportSecretKey"; import { PreferencesAutoLock } from "./Preferences/AutoLock"; import { PreferencesEthereum } from "./Preferences/Ethereum"; @@ -88,6 +91,10 @@ export function SettingsNavStackDrawer({ name="import-from-mnemonic" component={(props: any) => } /> + } + /> } diff --git a/packages/app-extension/src/components/common/WalletList.tsx b/packages/app-extension/src/components/common/WalletList.tsx index 1c55c3561..aad420e4f 100644 --- a/packages/app-extension/src/components/common/WalletList.tsx +++ b/packages/app-extension/src/components/common/WalletList.tsx @@ -48,7 +48,10 @@ import { CreateOrImportMnemonic, } from "../Unlocked/Settings/AddConnectWallet/CreateMnemonic"; import { ImportMenu } from "../Unlocked/Settings/AddConnectWallet/ImportMenu"; -import { ImportMnemonic } from "../Unlocked/Settings/AddConnectWallet/ImportMnemonic"; +import { + ImportMnemonic, + ImportMnemonicAutomatic, +} from "../Unlocked/Settings/AddConnectWallet/ImportMnemonic"; import { ImportSecretKey } from "../Unlocked/Settings/AddConnectWallet/ImportSecretKey"; import { RemoveWallet } from "../Unlocked/Settings/YourAccount/EditWallets/RemoveWallet"; import { RenameWallet } from "../Unlocked/Settings/YourAccount/EditWallets/RenameWallet"; @@ -254,6 +257,10 @@ function WalletNavStack({ name="create-or-import-mnemonic" component={(props: any) => } /> + } + /> } diff --git a/packages/background/src/backend/core.ts b/packages/background/src/backend/core.ts index 296ed9460..0bc29b037 100644 --- a/packages/background/src/backend/core.ts +++ b/packages/background/src/backend/core.ts @@ -1241,9 +1241,27 @@ export class Backend { mnemonic, recoveryPaths ); + + // + // The set of all keys currently in the keyring store. Don't try to sync + // a key if it's already client side. + // + const allLocalKeys = Object.values( + await this.keyringStoreReadAllPubkeys() + ) + .map((p) => + p.hdPublicKeys + .concat(p.importedPublicKeys) + .concat(p.ledgerPublicKeys) + .map((p) => p.publicKey) + ) + .reduce((a, b) => a.concat(b), []); + const searchPublicKeys = serverPublicKeys .filter((b) => b.blockchain === blockchain) - .map((p) => p.publicKey); + .map((p) => p.publicKey) + .filter((p) => !allLocalKeys.includes(p)); + for (const searchPublicKey of searchPublicKeys) { const index = publicKeys.findIndex( (p: string) => p === searchPublicKey @@ -1261,11 +1279,23 @@ export class Backend { // Doesn't exist, we can create it } if (blockchainKeyring) { - // Exists, just add the missing derivation path - const { publicKey, name } = - await this.keyringStore.activeUserKeyring - .keyringForBlockchain(blockchain) - .addDerivationPath(recoveryPaths[index]); + let [publicKey, name] = await (async () => { + const derivationPath = recoveryPaths[index]; + if (!blockchainKeyring.hasHdKeyring()) { + const [[publicKey, name]] = + await blockchainKeyring.initHdKeyring(mnemonic, [ + derivationPath, + ]); + return [publicKey, name]; + } else { + // Exists, just add the missing derivation path + const { publicKey, name } = + await this.keyringStore.activeUserKeyring + .keyringForBlockchain(blockchain) + .addDerivationPath(derivationPath); + return [publicKey, name]; + } + })(); this.events.emit(BACKEND_EVENT, { name: NOTIFICATION_KEYRING_DERIVED_WALLET, data: {