diff --git a/.changeset/rotten-suits-try.md b/.changeset/rotten-suits-try.md new file mode 100644 index 0000000000..ae2b6b87d8 --- /dev/null +++ b/.changeset/rotten-suits-try.md @@ -0,0 +1,5 @@ +--- +"talisman-ui": patch +--- + +fix: change container errors to normal casing diff --git a/apps/extension/src/@talisman/components/Accordion.tsx b/apps/extension/src/@talisman/components/Accordion.tsx index e4a4fa1b01..1ca4243734 100644 --- a/apps/extension/src/@talisman/components/Accordion.tsx +++ b/apps/extension/src/@talisman/components/Accordion.tsx @@ -24,11 +24,12 @@ export const AccordionIcon: FC<{ isOpen: boolean; className?: string }> = ({ ) -export const Accordion: FC<{ isOpen: boolean; children?: ReactNode; alwaysRender?: boolean }> = ({ - isOpen, - children, - alwaysRender, -}) => { +export const Accordion: FC<{ + isOpen: boolean + children?: ReactNode + alwaysRender?: boolean + className?: string +}> = ({ isOpen, children, alwaysRender, className }) => { const [contentHeight, setContentHeight] = useState() const refContainer = useRef(null) @@ -84,7 +85,7 @@ export const Accordion: FC<{ isOpen: boolean; children?: ReactNode; alwaysRender return ( { +export const addressFromSuri = (suri: string, type: KeypairType = "sr25519") => { // standalone/disposable keyring, this is not the one that stores user's keys const keyring = new Keyring({ type }) // create an account from mnemonic in it, just to get corresponding address // addFromUri supports mnemonic with optional appended derivation path - const { address } = keyring.addFromUri(mnemonicOrUri) + const { address } = keyring.addFromUri(suri) return address } diff --git a/apps/extension/src/@talisman/util/isValidDerivationPath.ts b/apps/extension/src/@talisman/util/isValidDerivationPath.ts new file mode 100644 index 0000000000..5e4b92623a --- /dev/null +++ b/apps/extension/src/@talisman/util/isValidDerivationPath.ts @@ -0,0 +1,21 @@ +import { formatSuri } from "@core/domains/accounts/helpers" +import { KeypairType } from "@polkadot/util-crypto/types" + +import { addressFromSuri } from "./addressFromSuri" + +const TEST_MNEMONIC = "test test test test test test test test test test test junk" + +/** + * + * Don't call this from front-end as it loads a heavy wasm blob + * + */ +export const isValidDerivationPath = (derivationPath: string, type: KeypairType) => { + if (typeof derivationPath !== "string") return false + try { + const suri = formatSuri(TEST_MNEMONIC, derivationPath) + return !!addressFromSuri(suri, type) + } catch (err) { + return false + } +} diff --git a/apps/extension/src/core/domains/accounts/handler.ts b/apps/extension/src/core/domains/accounts/handler.ts index ba8ab534ba..4cdc6fb9ac 100644 --- a/apps/extension/src/core/domains/accounts/handler.ts +++ b/apps/extension/src/core/domains/accounts/handler.ts @@ -1,4 +1,5 @@ import { + formatSuri, getNextDerivationPathForMnemonic, isValidAnyAddress, sortAccounts, @@ -18,7 +19,10 @@ import type { RequestAccountForget, RequestAccountRename, RequestAccountsCatalogAction, + RequestAddressLookup, + RequestNextDerivationPath, RequestSetVerifierCertificateMnemonic, + RequestValidateDerivationPath, ResponseAccountExport, } from "@core/domains/accounts/types" import { AccountTypes } from "@core/domains/accounts/types" @@ -35,7 +39,8 @@ import { KeyringPair$Meta } from "@polkadot/keyring/types" import keyring from "@polkadot/ui-keyring" import { assert } from "@polkadot/util" import { ethereumEncode, isEthereumAddress, mnemonicValidate } from "@polkadot/util-crypto" -import { addressFromMnemonic } from "@talisman/util/addressFromMnemonic" +import { addressFromSuri } from "@talisman/util/addressFromSuri" +import { isValidDerivationPath } from "@talisman/util/isValidDerivationPath" import { decodeAnyAddress, encodeAnyAddress, sleep } from "@talismn/util" import { combineLatest } from "rxjs" @@ -71,11 +76,24 @@ export default class AccountsHandler extends ExtensionHandler { mnemonic = options.mnemonic } - const { val: derivationPath, err } = getNextDerivationPathForMnemonic(mnemonic, type) - if (err) throw new Error(derivationPath) + let derivationPath: string + if (typeof options.derivationPath === "string") { + derivationPath = options.derivationPath + } else { + const { val, err } = getNextDerivationPathForMnemonic(mnemonic, type) + if (err) throw new Error(val) + else derivationPath = val + } + + const suri = formatSuri(mnemonic, derivationPath) + const resultingAddress = encodeAnyAddress(addressFromSuri(suri, type)) + assert( + allAccounts.every((acc) => encodeAnyAddress(acc.address) !== resultingAddress), + "Account already exists" + ) const { pair } = keyring.addUri( - `${mnemonic}${derivationPath}`, + suri, password, { name, @@ -92,17 +110,17 @@ export default class AccountsHandler extends ExtensionHandler { private async accountCreateSeed({ name, - seed: suri, // TODO split in 2 args: mnemonic and derivation path, or nuke the method and use only accountCreate with additional mnemonic arg + seed: suri, type, }: RequestAccountCreateFromSeed): Promise { const password = this.stores.password.getPassword() assert(password, "Not logged in") - const seedAddress = addressFromMnemonic(suri, type) + const expectedAddress = addressFromSuri(suri, type) const notExists = !keyring .getAccounts() - .some((acc) => acc.address.toLowerCase() === seedAddress.toLowerCase()) + .some((acc) => acc.address.toLowerCase() === expectedAddress.toLowerCase()) assert(notExists, "Account already exists") //suri includes the derivation path if any @@ -515,6 +533,43 @@ export default class AccountsHandler extends ExtensionHandler { return true } + private async addressLookup(lookup: RequestAddressLookup): Promise { + if ("mnemonicId" in lookup) { + const { mnemonicId, derivationPath, type } = lookup + + const password = this.stores.password.getPassword() + assert(password, "Not logged in") + const mnemonicResult = await this.stores.seedPhrase.getSeed(mnemonicId, password) + assert(mnemonicResult.ok && mnemonicResult.val, "Mnemonic not stored locally") + + const suri = formatSuri(mnemonicResult.val, derivationPath) + return addressFromSuri(suri, type) + } else { + const { suri, type } = lookup + return addressFromSuri(suri, type) + } + } + + private validateDerivationPath({ derivationPath, type }: RequestValidateDerivationPath): boolean { + return isValidDerivationPath(derivationPath, type) + } + + private async getNextDerivationPath({ + mnemonicId, + type, + }: RequestNextDerivationPath): Promise { + const password = this.stores.password.getPassword() + assert(password, "Not logged in") + + const { val: mnemonic, ok } = await this.stores.seedPhrase.getSeed(mnemonicId, password) + assert(ok && mnemonic, "Mnemonic not stored locally") + + const { val: derivationPath, ok: ok2 } = getNextDerivationPathForMnemonic(mnemonic, type) + assert(ok2, "Failed to lookup next available derivation path") + + return derivationPath + } + public async handle( id: string, type: TMessageType, @@ -556,8 +611,14 @@ export default class AccountsHandler extends ExtensionHandler { return this.accountsCatalogRunActions(request as RequestAccountsCatalogAction[]) case "pri(accounts.validateMnemonic)": return this.accountValidateMnemonic(request as string) + case "pri(accounts.validateDerivationPath)": + return this.validateDerivationPath(request as RequestValidateDerivationPath) + case "pri(accounts.address.lookup)": + return this.addressLookup(request as RequestAddressLookup) case "pri(accounts.setVerifierCertMnemonic)": return this.setVerifierCertMnemonic(request as RequestSetVerifierCertificateMnemonic) + case "pri(accounts.derivationPath.next)": + return this.getNextDerivationPath(request as RequestNextDerivationPath) default: throw new Error(`Unable to handle message of type ${type}`) } diff --git a/apps/extension/src/core/domains/accounts/helpers.ts b/apps/extension/src/core/domains/accounts/helpers.ts index abe1c89e70..3c6675e8ee 100644 --- a/apps/extension/src/core/domains/accounts/helpers.ts +++ b/apps/extension/src/core/domains/accounts/helpers.ts @@ -17,7 +17,7 @@ import type { SingleAddress, SubjectInfo } from "@polkadot/ui-keyring/observable import { hexToU8a, isHex } from "@polkadot/util" import { KeypairType } from "@polkadot/util-crypto/types" import { captureException } from "@sentry/browser" -import { addressFromMnemonic } from "@talisman/util/addressFromMnemonic" +import { addressFromSuri } from "@talisman/util/addressFromSuri" import { decodeAnyAddress, encodeAnyAddress } from "@talismn/util" import { Err, Ok, Result } from "ts-results" import Browser from "webextension-polyfill" @@ -168,7 +168,7 @@ export const getNextDerivationPathForMnemonic = ( try { // for substrate check empty derivation path first if (type !== "ethereum") { - const derivedAddress = encodeAnyAddress(addressFromMnemonic(mnemonic, type)) + const derivedAddress = encodeAnyAddress(addressFromSuri(mnemonic, type)) if (!allAccounts.some(({ address }) => encodeAnyAddress(address) === derivedAddress)) return Ok("") } @@ -178,9 +178,7 @@ export const getNextDerivationPathForMnemonic = ( for (let accountIndex = 0; accountIndex <= 1000; accountIndex += 1) { const derivationPath = getDerivationPath(accountIndex) - const derivedAddress = encodeAnyAddress( - addressFromMnemonic(`${mnemonic}${derivationPath}`, type) - ) + const derivedAddress = encodeAnyAddress(addressFromSuri(`${mnemonic}${derivationPath}`, type)) if (!allAccounts.some(({ address }) => encodeAnyAddress(address) === derivedAddress)) return Ok(derivationPath) @@ -231,3 +229,8 @@ export const isValidAnyAddress = (address: string) => { return false } } + +export const formatSuri = (mnemonic: string, derivationPath: string) => + derivationPath && !derivationPath.startsWith("/") + ? `${mnemonic}/${derivationPath}` + : `${mnemonic}${derivationPath}` diff --git a/apps/extension/src/core/domains/accounts/types.ts b/apps/extension/src/core/domains/accounts/types.ts index 50338da4ee..bf9fbd1a0f 100644 --- a/apps/extension/src/core/domains/accounts/types.ts +++ b/apps/extension/src/core/domains/accounts/types.ts @@ -181,10 +181,12 @@ export interface RequestAccountRename { export type RequestAccountCreateOptions = | { mnemonicId: string + derivationPath?: string } | { mnemonic: string confirmed: boolean + derivationPath?: string } export type RequestAccountCreate = { @@ -200,6 +202,28 @@ export type RequestSetVerifierCertificateMnemonic = { mnemonicId?: string } +// wrap in a dedicated type because empty strings are changed to null by the message service +export type RequestValidateDerivationPath = { + derivationPath: string + type: AccountAddressType +} + +export type RequestAddressLookupBySuri = { + suri: string + type: AccountAddressType +} +export type RequestAddressLookupByMnemonic = { + mnemonicId: string + derivationPath: string + type: AccountAddressType +} +export type RequestAddressLookup = RequestAddressLookupBySuri | RequestAddressLookupByMnemonic + +export type RequestNextDerivationPath = { + mnemonicId: string + type: AccountAddressType +} + export interface AccountsMessages { // account message signatures "pri(accounts.create)": [RequestAccountCreate, string] @@ -222,5 +246,8 @@ export interface AccountsMessages { "pri(accounts.catalog.subscribe)": [null, boolean, Trees] "pri(accounts.catalog.runActions)": [RequestAccountsCatalogAction[], boolean] "pri(accounts.validateMnemonic)": [string, boolean] + "pri(accounts.validateDerivationPath)": [RequestValidateDerivationPath, boolean] "pri(accounts.setVerifierCertMnemonic)": [RequestSetVerifierCertificateMnemonic, boolean] + "pri(accounts.address.lookup)": [RequestAddressLookup, string] + "pri(accounts.derivationPath.next)": [RequestNextDerivationPath, string] } diff --git a/apps/extension/src/core/domains/mnemonics/handler.ts b/apps/extension/src/core/domains/mnemonics/handler.ts index 032dc040a6..1d55690b99 100644 --- a/apps/extension/src/core/domains/mnemonics/handler.ts +++ b/apps/extension/src/core/domains/mnemonics/handler.ts @@ -1,7 +1,6 @@ import { ExtensionHandler } from "@core/libs/Handler" import { MessageTypes, RequestType, ResponseType } from "@core/types" import { assert } from "@polkadot/util" -import { addressFromMnemonic } from "@talisman/util/addressFromMnemonic" export default class MnemonicHandler extends ExtensionHandler { public async handle( @@ -26,11 +25,6 @@ export default class MnemonicHandler extends ExtensionHandler { return await this.stores.seedPhrase.setConfirmed(mnemonicId, confirmed) } - case "pri(mnemonic.address)": { - const { mnemonic, type } = request as RequestType<"pri(mnemonic.address)"> - return addressFromMnemonic(mnemonic, type) - } - case "pri(mnemonic.rename)": { const { mnemonicId, name } = request as RequestType<"pri(mnemonic.rename)"> return this.stores.seedPhrase.setName(mnemonicId, name) diff --git a/apps/extension/src/core/domains/mnemonics/types.ts b/apps/extension/src/core/domains/mnemonics/types.ts index 07a6a7d90f..da31f5be6a 100644 --- a/apps/extension/src/core/domains/mnemonics/types.ts +++ b/apps/extension/src/core/domains/mnemonics/types.ts @@ -1,14 +1,7 @@ -import { AccountAddressType } from "../accounts/types" - export declare type MnemonicSubscriptionResult = { confirmed?: boolean } -export declare type RequestAddressFromMnemonic = { - mnemonic: string - type?: AccountAddressType -} - type MnemonicId = string export declare type MnemonicUnlockRequest = { @@ -34,7 +27,6 @@ export interface MnemonicMessages { // mnemonic message signatures "pri(mnemonic.unlock)": [MnemonicUnlockRequest, string] "pri(mnemonic.confirm)": [MnemonicConfirmRequest, boolean] - "pri(mnemonic.address)": [RequestAddressFromMnemonic, string] "pri(mnemonic.rename)": [MnemonicRenameRequest, boolean] "pri(mnemonic.delete)": [MnemonicDeleteRequest, boolean] } diff --git a/apps/extension/src/core/handlers/index.ts b/apps/extension/src/core/handlers/index.ts index 679ca7b8b5..2a4b767dac 100644 --- a/apps/extension/src/core/handlers/index.ts +++ b/apps/extension/src/core/handlers/index.ts @@ -25,6 +25,7 @@ const OBFUSCATE_LOG_MESSAGES: MessageTypes[] = [ "pri(accounts.create.seed)", "pri(accounts.create.json)", "pri(accounts.setVerifierCertMnemonic)", + "pri(accounts.address.lookup)", "pri(app.onboardCreatePassword)", ] const OBFUSCATED_PAYLOAD = "#OBFUSCATED#" diff --git a/apps/extension/src/ui/api/api.ts b/apps/extension/src/ui/api/api.ts index bec602ac86..2c3b56c541 100644 --- a/apps/extension/src/ui/api/api.ts +++ b/apps/extension/src/ui/api/api.ts @@ -76,8 +76,6 @@ export const api: MessageTypes = { messageService.sendMessage("pri(mnemonic.rename)", { mnemonicId, name }), mnemonicDelete: (mnemonicId) => messageService.sendMessage("pri(mnemonic.delete)", { mnemonicId }), - addressFromMnemonic: (mnemonic, type) => - messageService.sendMessage("pri(mnemonic.address)", { mnemonic, type }), // account messages --------------------------------------------------- accountCreate: (name, type, options) => @@ -136,12 +134,17 @@ export const api: MessageTypes = { messageService.sendMessage("pri(accounts.external.setIsPortfolio)", { address, isPortfolio }), accountValidateMnemonic: (mnemonic) => messageService.sendMessage("pri(accounts.validateMnemonic)", mnemonic), + validateDerivationPath: (derivationPath, type) => + messageService.sendMessage("pri(accounts.validateDerivationPath)", { derivationPath, type }), + addressLookup: (lookup) => messageService.sendMessage("pri(accounts.address.lookup)", lookup), setVerifierCertMnemonic: (verifierCertType, mnemonic, mnemonicId) => messageService.sendMessage("pri(accounts.setVerifierCertMnemonic)", { type: verifierCertType, mnemonic, mnemonicId, }), + getNextDerivationPath: (mnemonicId, type) => + messageService.sendMessage("pri(accounts.derivationPath.next)", { mnemonicId, type }), // balance messages --------------------------------------------------- getBalance: ({ chainId, evmNetworkId, tokenId, address }) => diff --git a/apps/extension/src/ui/api/types.ts b/apps/extension/src/ui/api/types.ts index b479b80ecd..67daaf85af 100644 --- a/apps/extension/src/ui/api/types.ts +++ b/apps/extension/src/ui/api/types.ts @@ -4,6 +4,7 @@ import type { AccountJson, RequestAccountCreateOptions, RequestAccountsCatalogAction, + RequestAddressLookup, VerifierCertificateType, } from "@core/domains/accounts/types" import { @@ -109,7 +110,6 @@ export default interface MessageTypes { mnemonicConfirm: (mnemonicId: string, confirmed: boolean) => Promise mnemonicRename: (mnemonicId: string, name: string) => Promise mnemonicDelete: (mnemonicId: string) => Promise - addressFromMnemonic: (mnemonic: string, type?: AccountAddressType) => Promise // account message types --------------------------------------------------- accountCreate: ( @@ -145,6 +145,9 @@ export default interface MessageTypes { accountExportPrivateKey: (address: string, password: string) => Promise accountRename: (address: string, name: string) => Promise accountValidateMnemonic: (mnemonic: string) => Promise + validateDerivationPath: (derivationPath: string, type: AccountAddressType) => Promise + addressLookup: (lookup: RequestAddressLookup) => Promise + getNextDerivationPath: (mnemonicId: string, type: AccountAddressType) => Promise setVerifierCertMnemonic: ( type: VerifierCertificateType, mnemonic?: string, diff --git a/apps/extension/src/ui/apps/dashboard/routes/AccountAdd/AccountAddDerivedPage.tsx b/apps/extension/src/ui/apps/dashboard/routes/AccountAdd/AccountAddDerivedPage.tsx index 6c6fce194c..f3b64dffff 100644 --- a/apps/extension/src/ui/apps/dashboard/routes/AccountAdd/AccountAddDerivedPage.tsx +++ b/apps/extension/src/ui/apps/dashboard/routes/AccountAdd/AccountAddDerivedPage.tsx @@ -1,7 +1,7 @@ import { AccountAddressType } from "@core/domains/accounts/types" import { HeaderBlock } from "@talisman/components/HeaderBlock" import { Spacer } from "@talisman/components/Spacer" -import { AccountAddDerivedForm } from "@ui/domains/Account/AccountAdd/AccountAddDerivedForm" +import { AccountAddDerivedForm } from "@ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm" import { useSelectAccountAndNavigate } from "@ui/hooks/useSelectAccountAndNavigate" import { useTranslation } from "react-i18next" import { useSearchParams } from "react-router-dom" diff --git a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddDerivedPage.tsx b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddDerivedPage.tsx index 0abe6ceeaa..588b9a45ae 100644 --- a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddDerivedPage.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddDerivedPage.tsx @@ -1,10 +1,15 @@ -import { AccountAddDerivedForm } from "@ui/domains/Account/AccountAdd/AccountAddDerivedForm" +import { AccountAddDerivedForm } from "@ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm" +import { useTranslation } from "react-i18next" import { AccountAddWrapper } from "./AccountAddWrapper" -export const AccountAddDerivedPage = () => ( - } - /> -) +export const AccountAddDerivedPage = () => { + const { t } = useTranslation("admin") + + return ( + } + /> + ) +} diff --git a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddJsonPage.tsx b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddJsonPage.tsx index a1a5861c62..c4cc0ad8ad 100644 --- a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddJsonPage.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddJsonPage.tsx @@ -1,11 +1,15 @@ import { AccountAddJson } from "@ui/domains/Account/AccountAdd/AccountAddJson" +import { useTranslation } from "react-i18next" import { AccountAddWrapper } from "./AccountAddWrapper" -export const AccountAddJsonOnboardPage = () => ( - } - /> -) +export const AccountAddJsonOnboardPage = () => { + const { t } = useTranslation("admin") + return ( + } + /> + ) +} diff --git a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWatchedPage.tsx b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWatchedPage.tsx index ce56576069..20c02b65cd 100644 --- a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWatchedPage.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWatchedPage.tsx @@ -1,11 +1,15 @@ import { AccountAddWatchedForm } from "@ui/domains/Account/AccountAdd/AccountAddWatchedForm" +import { useTranslation } from "react-i18next" import { AccountAddWrapper } from "./AccountAddWrapper" -export const AccountAddWatchedPage = () => ( - } - /> -) +export const AccountAddWatchedPage = () => { + const { t } = useTranslation("admin") + return ( + } + /> + ) +} diff --git a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWrapper.tsx b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWrapper.tsx index 6094e9ef34..6c64e22d8c 100644 --- a/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWrapper.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/AddAccount/AccountAddWrapper.tsx @@ -1,7 +1,6 @@ import { OnboardDialog } from "@ui/apps/onboard/components/OnboardDialog" import { useOnboard } from "@ui/apps/onboard/context" import { Layout } from "@ui/apps/onboard/layout" -import { useTranslation } from "react-i18next" export const AccountAddWrapper = ({ title, @@ -12,13 +11,12 @@ export const AccountAddWrapper = ({ subtitle?: string render: (onSuccess: (address: string) => void) => JSX.Element }) => { - const { t } = useTranslation("onboard") const { setOnboarded } = useOnboard() return ( - - {subtitle &&

{t(subtitle)}

} + + {subtitle &&

{subtitle}

} {render(setOnboarded)}
diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx new file mode 100644 index 0000000000..076563d988 --- /dev/null +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddDerivedForm.tsx @@ -0,0 +1,326 @@ +import { AccountAddressType, RequestAccountCreateOptions } from "@core/domains/accounts/types" +import { log } from "@core/log" +import { yupResolver } from "@hookform/resolvers/yup" +import { Accordion, AccordionIcon } from "@talisman/components/Accordion" +import { notify, notifyUpdate } from "@talisman/components/Notifications" +import { Spacer } from "@talisman/components/Spacer" +import { ArrowRightIcon } from "@talisman/theme/icons" +import { classNames } from "@talismn/util" +import { sleep } from "@talismn/util" +import { useQuery } from "@tanstack/react-query" +import { api } from "@ui/api" +import { AccountTypeSelector } from "@ui/domains/Account/AccountTypeSelector" +import { + MnemonicCreateModal, + MnemonicCreateModalProvider, + useMnemonicCreateModal, +} from "@ui/domains/Mnemonic/MnemonicCreateModal" +import useAccounts from "@ui/hooks/useAccounts" +import { useMnemonics } from "@ui/hooks/useMnemonics" +import { FC, PropsWithChildren, useCallback, useEffect, useMemo } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useSearchParams } from "react-router-dom" +import { + Button, + Checkbox, + FormFieldContainer, + FormFieldInputText, + Tooltip, + TooltipContent, + TooltipTrigger, + useOpenClose, +} from "talisman-ui" +import * as yup from "yup" + +import { AccountIcon } from "../../AccountIcon" +import { AccountAddPageProps } from "../types" +import { AccountAddMnemonicDropdown } from "./AccountAddMnemonicDropdown" + +type FormData = { + name: string + type: AccountAddressType + mnemonicId: string | null + customDerivationPath: boolean + derivationPath: string +} + +const useNextAvailableDerivationPath = (mnemonicId: string | null, type: AccountAddressType) => { + return useQuery({ + queryKey: ["useNextAvailableDerivationPath", mnemonicId, type], + queryFn: () => { + if (!mnemonicId || !type) return null + return api.getNextDerivationPath(mnemonicId, type) + }, + enabled: !!mnemonicId, + refetchInterval: false, + retry: false, + }) +} + +const useLookupAddress = ( + mnemonicId: string | null, + type: AccountAddressType, + derivationPath: string | null | undefined +) => { + return useQuery({ + queryKey: ["useLookupAddress", mnemonicId, derivationPath], + queryFn: async () => { + // empty string is valid + if (!mnemonicId || !type || typeof derivationPath !== "string") return null + if (!(await api.validateDerivationPath(derivationPath, type))) return null + return api.addressLookup({ mnemonicId, type, derivationPath }) + }, + enabled: !!mnemonicId && type && typeof derivationPath === "string", + refetchInterval: false, + retry: false, + }) +} + +const AdvancedSettings: FC = ({ children }) => { + const { t } = useTranslation("admin") + const { toggle, isOpen } = useOpenClose() + + return ( +
+
+ +
+ {/* enlarge the area or it would hide focus ring on the inputs */} + + {children} + +
+ ) +} + +const AccountAddDerivedFormInner: FC = ({ onSuccess }) => { + const { t } = useTranslation("admin") + // get type paramter from url + const [params] = useSearchParams() + const urlParamType = (params.get("type") ?? undefined) as AccountAddressType | undefined + const mnemonics = useMnemonics() + const allAccounts = useAccounts() + const accountNames = useMemo(() => allAccounts.map((a) => a.name), [allAccounts]) + + const schema = useMemo( + () => + yup + .object({ + name: yup.string().required("").notOneOf(accountNames, t("Name already in use")), + type: yup.string().required("").oneOf(["ethereum", "sr25519"]), + derivationPath: yup.string(), + }) + .required() + .test("validateDerivationPath", t("Invalid derivation path"), async (val, ctx) => { + const { customDerivationPath, derivationPath, mnemonicId, type } = val as FormData + if (!customDerivationPath) return true + + if (!(await api.validateDerivationPath(derivationPath, type))) + return ctx.createError({ + path: "derivationPath", + message: t("Invalid derivation path"), + }) + + if (mnemonicId) { + const address = await api.addressLookup({ mnemonicId, derivationPath, type }) + if (allAccounts.some((a) => a.address === address)) + return ctx.createError({ + path: "derivationPath", + message: t("Account already exists"), + }) + } + return true + }), + [accountNames, t, allAccounts] + ) + + const { + register, + handleSubmit, + setValue, + setFocus, + watch, + formState: { errors, isValid, isSubmitting }, + } = useForm({ + mode: "onChange", + resolver: yupResolver(schema), + defaultValues: { type: urlParamType, mnemonicId: mnemonics[0]?.id ?? null, derivationPath: "" }, + }) + + const { generateMnemonic } = useMnemonicCreateModal() + + const submit = useCallback( + async ({ name, type, mnemonicId, customDerivationPath, derivationPath }: FormData) => { + let options: RequestAccountCreateOptions + + // note on derivation path : + // undefined : backend will use next available derivation path + // string : forces backend to use provided value, empty string being a valid derivation path + + if (mnemonicId === null) { + const mnemonicOptions = await generateMnemonic() + if (mnemonicOptions === null) return // cancelled + options = { + ...mnemonicOptions, + derivationPath: customDerivationPath ? derivationPath : undefined, + } + } else { + options = { + mnemonicId, // undefined and empty strings should not be treated the same + derivationPath: customDerivationPath ? derivationPath : undefined, + } + } + + const notificationId = notify( + { + type: "processing", + title: t("Creating account"), + subtitle: t("Please wait"), + }, + { autoClose: false } + ) + + try { + // pause to prevent double notification + await sleep(1000) + + onSuccess(await api.accountCreate(name, type, options)) + + notifyUpdate(notificationId, { + type: "success", + title: t("Account created"), + subtitle: name, + }) + } catch (err) { + log.error("Failed to create account", err) + notifyUpdate(notificationId, { + type: "error", + title: t("Error creating account"), + subtitle: (err as Error)?.message, + }) + } + }, + [generateMnemonic, onSuccess, t] + ) + + const handleTypeChange = useCallback( + (type: AccountAddressType) => { + setValue("type", type, { shouldValidate: true }) + setFocus("name") + }, + [setFocus, setValue] + ) + + const handleMnemonicChange = useCallback( + (mnemonicId: string | null) => { + setValue("mnemonicId", mnemonicId, { shouldValidate: true }) + }, + [setValue] + ) + + const { type, mnemonicId, customDerivationPath, derivationPath } = watch() + const { data: nextDerivationPath } = useNextAvailableDerivationPath(mnemonicId, type) + const { data: address } = useLookupAddress( + mnemonicId, + type, + customDerivationPath ? derivationPath : nextDerivationPath + ) + + useEffect(() => { + // prefill custom derivation path with next available one + if (nextDerivationPath === undefined || nextDerivationPath === null) return + if (!customDerivationPath) + setValue("derivationPath", nextDerivationPath, { shouldValidate: true }) + }, [customDerivationPath, nextDerivationPath, setValue]) + + useEffect(() => { + // if we have a type in the url, set it + if (urlParamType) handleTypeChange(urlParamType) + }, [urlParamType, handleTypeChange]) + + return ( +
+ + +
+ {!!mnemonics.length && ( + + )} + + + + + + {address} + + ) : null + } + /> + + + + + {t("Custom derivation path")} + + + + + + +
+ +
+
+ + ) +} + +export const AccountAddDerivedForm: FC = ({ onSuccess }) => { + return ( + + + + + ) +} diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddMnemonicDropdown.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddMnemonicDropdown.tsx new file mode 100644 index 0000000000..3fbaec4ffb --- /dev/null +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerived/AccountAddMnemonicDropdown.tsx @@ -0,0 +1,87 @@ +import { AccountJsonAny } from "@core/domains/accounts/types" +import { PlusIcon, SeedIcon } from "@talisman/theme/icons" +import useAccounts from "@ui/hooks/useAccounts" +import { useMnemonics } from "@ui/hooks/useMnemonics" +import { FC, useCallback, useMemo } from "react" +import { useTranslation } from "react-i18next" +import { Dropdown } from "talisman-ui" + +export type MnemonicOption = { + value: string + label: string + accounts?: AccountJsonAny[] +} + +const GENERATE_MNEMONIC_OPTION = { + value: "new", + label: "Generate new recovery phrase", + accountsCount: undefined, +} + +export const AccountAddMnemonicDropdown: FC<{ + value: string | null // null means "generate new" + onChange: (mnemonicId: string | null) => void +}> = ({ value, onChange }) => { + const { t } = useTranslation("admin") + + const allAccounts = useAccounts() + + const mnemonics = useMnemonics() + const mnemonicOptions: MnemonicOption[] = useMemo(() => { + const accountsByMnemonic = allAccounts.reduce((result, acc) => { + if (!acc.derivedMnemonicId) return result + if (!result[acc.derivedMnemonicId]) result[acc.derivedMnemonicId] = [] + result[acc.derivedMnemonicId].push(acc) + return result + }, {} as Record) + return [ + ...mnemonics + .map((m) => ({ + label: m.name, + value: m.id, + accounts: accountsByMnemonic[m.id] || [], + })) + .sort((a, b) => a.label.localeCompare(b.label)), + GENERATE_MNEMONIC_OPTION, + ] + }, [allAccounts, mnemonics]) + + const selected = useMemo( + () => mnemonicOptions.find((o) => o.value === value) ?? GENERATE_MNEMONIC_OPTION, + [mnemonicOptions, value] + ) + + const handleChange = useCallback( + (o: MnemonicOption | null) => { + if (!o) return // shouldn't happen + onChange(o.value === "new" ? null : o.value) + }, + [onChange] + ) + + return ( + ( +
+
+ {o.value === "new" ? : } +
+
{o.label}
+ {o.value !== "new" && ( +
+ {t("used by {{count}} accounts", { count: o.accounts?.length ?? 0 })} +
+ )} +
+ )} + value={selected} + onChange={handleChange} + buttonClassName="py-6 bg-field" + optionClassName="py-4 bg-field" + /> + ) +} diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerivedForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerivedForm.tsx deleted file mode 100644 index 9f270226c6..0000000000 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddDerivedForm.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { - AccountAddressType, - AccountJsonAny, - RequestAccountCreateOptions, -} from "@core/domains/accounts/types" -import { log } from "@core/log" -import { yupResolver } from "@hookform/resolvers/yup" -import { notify, notifyUpdate } from "@talisman/components/Notifications" -import { Spacer } from "@talisman/components/Spacer" -import { ArrowRightIcon, PlusIcon, SeedIcon } from "@talisman/theme/icons" -import { classNames } from "@talismn/util" -import { sleep } from "@talismn/util" -import { api } from "@ui/api" -import { AccountTypeSelector } from "@ui/domains/Account/AccountTypeSelector" -import { - MnemonicCreateModal, - MnemonicCreateModalProvider, - useMnemonicCreateModal, -} from "@ui/domains/Mnemonic/MnemonicCreateModal" -import useAccounts from "@ui/hooks/useAccounts" -import { useMnemonics } from "@ui/hooks/useMnemonics" -import { FC, useCallback, useEffect, useMemo, useState } from "react" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { useSearchParams } from "react-router-dom" -import { Button, Dropdown, FormFieldContainer, FormFieldInputText } from "talisman-ui" -import * as yup from "yup" - -import { AccountAddPageProps } from "./types" - -type MnemonicOption = { - value: string - label: string - accounts?: AccountJsonAny[] -} - -const GENERATE_MNEMONIC_OPTION = { - value: "new", - label: "Generate new recovery phrase", - accountsCount: undefined, -} - -type FormData = { - name: string - type: AccountAddressType -} - -const AccountAddDerivedFormInner: FC = ({ onSuccess }) => { - const { t } = useTranslation("admin") - // get type paramter from url - const [params] = useSearchParams() - const urlParamType = (params.get("type") ?? undefined) as AccountAddressType | undefined - const allAccounts = useAccounts() - const accountNames = useMemo(() => allAccounts.map((a) => a.name), [allAccounts]) - - const schema = useMemo( - () => - yup - .object({ - name: yup.string().required("").notOneOf(accountNames, t("Name already in use")), - type: yup.string().required("").oneOf(["ethereum", "sr25519"]), - }) - .required(), - - [accountNames, t] - ) - - const { - register, - handleSubmit, - setValue, - setFocus, - watch, - formState: { errors, isValid, isSubmitting }, - } = useForm({ - mode: "onChange", - resolver: yupResolver(schema), - defaultValues: { type: urlParamType }, - }) - - const mnemonics = useMnemonics() - const mnemonicOptions: MnemonicOption[] = useMemo(() => { - const accountsByMnemonic = allAccounts.reduce((result, acc) => { - if (!acc.derivedMnemonicId) return result - if (!result[acc.derivedMnemonicId]) result[acc.derivedMnemonicId] = [] - result[acc.derivedMnemonicId].push(acc) - return result - }, {} as Record) - return [ - ...mnemonics - .map((m) => ({ - label: m.name, - value: m.id, - accounts: accountsByMnemonic[m.id] || [], - })) - .sort((a, b) => a.label.localeCompare(b.label)), - GENERATE_MNEMONIC_OPTION, - ] - }, [allAccounts, mnemonics]) - const [selectedMnemonic, setSelectedMnemonic] = useState( - () => mnemonicOptions[0] - ) - - const { generateMnemonic } = useMnemonicCreateModal() - - const submit = useCallback( - async ({ name, type }: FormData) => { - if (!selectedMnemonic) return - - let options: RequestAccountCreateOptions - - if (selectedMnemonic.value === "new") { - const mnemonicOptions = await generateMnemonic() - if (mnemonicOptions === null) return // cancelled - options = mnemonicOptions - } else { - options = { mnemonicId: selectedMnemonic.value } - } - - const notificationId = notify( - { - type: "processing", - title: t("Creating account"), - subtitle: t("Please wait"), - }, - { autoClose: false } - ) - - try { - // pause to prevent double notification - await sleep(1000) - - onSuccess(await api.accountCreate(name, type, options)) - - notifyUpdate(notificationId, { - type: "success", - title: t("Account created"), - subtitle: name, - }) - } catch (err) { - log.error("Failed to create account", err) - notifyUpdate(notificationId, { - type: "error", - title: t("Error creating account"), - subtitle: (err as Error)?.message, - }) - } - }, - [generateMnemonic, onSuccess, selectedMnemonic, t] - ) - - const handleTypeChange = useCallback( - (type: AccountAddressType) => { - setValue("type", type, { shouldValidate: true }) - setFocus("name") - }, - [setFocus, setValue] - ) - - const type = watch("type") - - useEffect(() => { - // if we have a type in the url, set it - if (urlParamType) handleTypeChange(urlParamType) - }, [urlParamType, handleTypeChange]) - - return ( -
- - -
- {mnemonicOptions.length > 1 && ( - ( -
-
- {o.value === "new" ? : } -
-
{o.label}
- {o.value !== "new" && ( -
- {t("used by {{count}} accounts", { count: o.accounts?.length ?? 0 })} -
- )} -
- )} - value={selectedMnemonic} - onChange={setSelectedMnemonic} - buttonClassName="py-6 bg-field" - optionClassName="py-4 bg-field" - /> - )} - - - - -
- -
-
- - ) -} - -export const AccountAddDerivedForm: FC = ({ onSuccess }) => { - return ( - - - - - ) -} diff --git a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddSecret/AccountAddSecretMnemonicForm.tsx b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddSecret/AccountAddSecretMnemonicForm.tsx index c3fabdf9d4..7c743bd445 100644 --- a/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddSecret/AccountAddSecretMnemonicForm.tsx +++ b/apps/extension/src/ui/domains/Account/AccountAdd/AccountAddSecret/AccountAddSecretMnemonicForm.tsx @@ -1,10 +1,11 @@ import { AccountAddressType } from "@core/domains/accounts/types" import { getEthDerivationPath } from "@core/domains/ethereum/helpers" import { yupResolver } from "@hookform/resolvers/yup" +import { mnemonicValidate } from "@polkadot/util-crypto" import { HeaderBlock } from "@talisman/components/HeaderBlock" import { notify, notifyUpdate } from "@talisman/components/Notifications" import { Spacer } from "@talisman/components/Spacer" -import { classNames } from "@talismn/util" +import { classNames, encodeAnyAddress } from "@talismn/util" import { api } from "@ui/api" import { AccountIcon } from "@ui/domains/Account/AccountIcon" import { AccountTypeSelector } from "@ui/domains/Account/AccountTypeSelector" @@ -16,21 +17,17 @@ import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { Button, - Checkbox, FormFieldContainer, FormFieldInputText, FormFieldTextarea, + Tooltip, + TooltipContent, + TooltipTrigger, } from "talisman-ui" import * as yup from "yup" -import { useAccountAddSecret } from "./context" - -type FormData = { - name: string - type: AccountAddressType - mnemonic: string - multi: boolean -} +import { AccountAddDerivationMode, useAccountAddSecret } from "./context" +import { DerivationModeDropdown } from "./DerivationModeDropdown" const cleanupMnemonic = (input = "") => input @@ -51,42 +48,27 @@ const isValidEthPrivateKey = (privateKey?: string) => { } } -// for polkadot, do not force //0 derivation path to preserve backwards compatibility (since beta we import mnemonics as-is) -// but for ethereum, use metamask's derivation path -const ETHEREUM_DERIVATION_PATH = getEthDerivationPath() - -const getAccountUri = async (secret: string, type: AccountAddressType) => { - if (!secret || !type) throw new Error("Missing arguments") +const getSuri = (secret: string, type: AccountAddressType, derivationPath?: string) => { + if (!secret || !type) return null // metamask exports private key without the 0x in front of it // pjs keyring & crypto api will throw if it's missing if (type === "ethereum" && isValidEthPrivateKey(secret)) return secret.startsWith("0x") ? secret : `0x${secret}` - if (await testValidMnemonic(secret)) - return type === "ethereum" ? `${secret}${ETHEREUM_DERIVATION_PATH}` : secret - throw new Error("Invalid recovery phrase") -} + if (!mnemonicValidate(secret)) return null -const testNoDuplicate = async ( - allAccountsAddresses: string[], - type: AccountAddressType, - mnemonic?: string -) => { - if (!mnemonic) return false - try { - const uri = await getAccountUri(mnemonic, type) - const address = await api.addressFromMnemonic(uri, type) - return !allAccountsAddresses.includes(address) - } catch (err) { - return false - } + return derivationPath && !derivationPath.startsWith("/") + ? `${secret}/${derivationPath}` + : `${secret}${derivationPath}` } -const testValidMnemonic = async (val?: string) => { - // Don't bother calling the api if the mnemonic isn't the right length to reduce Sentry noise - if (!val || ![12, 24].includes(val.split(" ").length)) return false - return await api.accountValidateMnemonic(val) +type FormData = { + name: string + type: AccountAddressType + mnemonic: string + mode: AccountAddDerivationMode + derivationPath: string } export const AccountAddSecretMnemonicForm = () => { @@ -104,7 +86,8 @@ export const AccountAddSecretMnemonicForm = () => { .object({ name: yup.string().trim().required(""), type: yup.string().required("").oneOf(["ethereum", "sr25519"]), - multi: yup.boolean(), + mode: yup.string().required("").oneOf(["first", "custom", "multi"]), + derivationPath: yup.string().trim(), mnemonic: yup .string() .trim() @@ -117,33 +100,42 @@ export const AccountAddSecretMnemonicForm = () => { .test( "is-valid-mnemonic-ethereum", t("Invalid secret"), - (val) => isValidEthPrivateKey(val) || testValidMnemonic(val) - ) - .when("multi", { - is: false, - then: yup - .string() - .test("not-duplicate-ethereum", t("Account already exists"), async (val) => - testNoDuplicate(accountAddresses, "ethereum", val) - ), - }), + async (val) => isValidEthPrivateKey(val) || api.accountValidateMnemonic(val ?? "") + ), otherwise: yup .string() .test("is-valid-mnemonic-sr25519", t("Invalid secret"), (val) => - testValidMnemonic(val) - ) - .when("multi", { - is: false, - then: yup - .string() - .test("not-duplicate-sr25519", t("Account already exists"), async (val) => - testNoDuplicate(accountAddresses, "sr25519", val) - ), - }), + api.accountValidateMnemonic(val ?? "") + ), }), }) - .required(), - [t, accountAddresses] + .required() + .test("account-exists", t("Account exists"), async (val, ctx) => { + const { mnemonic, type, derivationPath, mode } = val as FormData + if (!val || mode === "multi") return true + + const suri = getSuri(mnemonic, type, derivationPath) + if (!suri) return true + + let address: string + try { + address = await api.addressLookup({ suri, type }) + } catch (err) { + return ctx.createError({ + path: "derivationPath", + message: t("Invalid derivation path"), + }) + } + + if (accountAddresses.some((a) => encodeAnyAddress(a) === address)) + return ctx.createError({ + path: mode === "custom" ? "derivationPath" : "mnemonic", + message: t("Account already exists"), + }) + + return true + }), + [accountAddresses, t] ) const { @@ -159,14 +151,14 @@ export const AccountAddSecretMnemonicForm = () => { resolver: yupResolver(schema), }) - const { type, mnemonic } = watch() + const { type, mnemonic, mode, derivationPath } = watch() const isPrivateKey = useMemo( () => type === "ethereum" && isValidEthPrivateKey(mnemonic), [mnemonic, type] ) useEffect(() => { - if (isPrivateKey) setValue("multi", false, { shouldValidate: true }) + if (isPrivateKey) setValue("mode", "first", { shouldValidate: true }) }, [isPrivateKey, setValue]) const words = useMemo( @@ -179,21 +171,25 @@ export const AccountAddSecretMnemonicForm = () => { useEffect(() => { const refreshTargetAddress = async () => { try { - const uri = await getAccountUri(cleanupMnemonic(mnemonic), type) - setTargetAddress(await api.addressFromMnemonic(uri, type)) + const suri = getSuri(cleanupMnemonic(mnemonic), type, derivationPath) + if (!suri) return setTargetAddress(undefined) + setTargetAddress(await api.addressLookup({ suri, type })) } catch (err) { setTargetAddress(undefined) } } refreshTargetAddress() - }, [isValid, mnemonic, type]) + }, [derivationPath, isValid, mnemonic, type]) const submit = useCallback( - async ({ type, name, mnemonic, multi }: FormData) => { - updateData({ type, name, mnemonic, multi }) - if (multi) navigate("multiple") + async ({ type, name, mnemonic, mode, derivationPath }: FormData) => { + updateData({ type, name, mnemonic, mode, derivationPath }) + if (mode === "multi") navigate("multiple") else { + const suri = getSuri(mnemonic, type, derivationPath) + if (!suri) return + const notificationId = notify( { type: "processing", @@ -203,8 +199,7 @@ export const AccountAddSecretMnemonicForm = () => { { autoClose: false } ) try { - const uri = await getAccountUri(mnemonic, type) - onSuccess(await api.accountCreateFromSeed(name, uri, type)) + onSuccess(await api.accountCreateFromSeed(name, suri, type)) notifyUpdate(notificationId, { type: "success", title: t("Account imported"), @@ -225,12 +220,33 @@ export const AccountAddSecretMnemonicForm = () => { const handleTypeChange = useCallback( (type: AccountAddressType) => { setValue("type", type, { shouldValidate: true }) + if (mode === "first") + setValue("derivationPath", type === "ethereum" ? getEthDerivationPath() : "", { + shouldValidate: true, + }) // revalidate to get rid of "invalid mnemonic" with a private key, when switching to ethereum trigger() }, - [setValue, trigger] + [mode, setValue, trigger] + ) + + const handleModeChange = useCallback( + (mode: AccountAddDerivationMode) => { + setValue("mode", mode, { shouldValidate: true }) + if (mode === "first") + setValue("derivationPath", type === "ethereum" ? getEthDerivationPath() : "", { + shouldValidate: true, + }) + }, + [setValue, type] ) + useEffect(() => { + setValue("derivationPath", type === "ethereum" ? getEthDerivationPath() : "", { + shouldValidate: true, + }) + }, [setValue, type]) + return (
{ autoFocus data-lpignore after={ - targetAddress ? : null + targetAddress ? ( + + + + + {targetAddress} + + ) : null } /> @@ -266,17 +289,25 @@ export const AccountAddSecretMnemonicForm = () => { data-lpignore spellCheck={false} /> -
-
{t("Word count: {{words}}", { words })}
-
{errors.mnemonic?.message}
+
+
{t("Word count: {{words}}", { words })}
+
{errors.mnemonic?.message}
- + - {t("Import multiple accounts from this recovery phrase")} - + +
diff --git a/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx b/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx index 0b86147237..71fc13cb62 100644 --- a/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx +++ b/apps/extension/src/ui/domains/Account/DerivedFromMnemonicAccountPicker.tsx @@ -1,3 +1,4 @@ +import { formatSuri } from "@core/domains/accounts/helpers" import { AccountAddressType, RequestAccountCreateFromSeed } from "@core/domains/accounts/types" import { AddressesAndEvmNetwork } from "@core/domains/balances/types" import { getEthDerivationPath } from "@core/domains/ethereum/helpers" @@ -49,14 +50,14 @@ const useDerivedAccounts = ( // maps [0, 1, 2, ..., itemsPerPage - 1] dynamically Array.from(Array(itemsPerPage).keys()).map(async (i) => { const accountIndex = skip + i - const seed = mnemonic + getDerivationPath(type, accountIndex) - const rawAddress = await api.addressFromMnemonic(seed, type) + const suri = formatSuri(mnemonic, getDerivationPath(type, accountIndex)) + const rawAddress = await api.addressLookup({ suri, type }) const address = type === "ethereum" ? rawAddress : convertAddress(rawAddress, 0) return { accountIndex, name: `${name}${accountIndex === 0 ? "" : ` ${accountIndex}`}`, - seed, + seed: suri, type, address, } as DerivedFromMnemonicAccount diff --git a/packages/talisman-ui/src/components/FormFieldContainer.tsx b/packages/talisman-ui/src/components/FormFieldContainer.tsx index b1be3218c3..a6373b8c73 100644 --- a/packages/talisman-ui/src/components/FormFieldContainer.tsx +++ b/packages/talisman-ui/src/components/FormFieldContainer.tsx @@ -21,7 +21,7 @@ export const FormFieldContainer: FC = ({
{label}
{children}
{!noErrorRow && ( -
+
{error}
)}