Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom derivation path - 2nd attempt #1049

Merged
merged 14 commits into from
Sep 6, 2023
5 changes: 5 additions & 0 deletions .changeset/rotten-suits-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"talisman-ui": patch
---

fix: change container errors to normal casing
13 changes: 7 additions & 6 deletions apps/extension/src/@talisman/components/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ export const AccordionIcon: FC<{ isOpen: boolean; className?: string }> = ({
</div>
)

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<number>()
const refContainer = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -84,7 +85,7 @@ export const Accordion: FC<{ isOpen: boolean; children?: ReactNode; alwaysRender

return (
<motion.div
className="overflow-y-hidden"
className={classNames("overflow-y-hidden", className)}
style={style}
ref={refContainer}
animate={animate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { KeypairType } from "@polkadot/util-crypto/types"

/**
*
* Don't call this from front-end as it imports heavy polkadot crypto libs
* Don't call this from front-end as it loads a heavy wasm blob
*
* @param mnemonicOrUri mnemonic with optionally appended derivation path
* @param suri Substrate URI : mnemonic with optionally appended derivation path
* @param type
* @returns address of the first keypair associated to a mnemonic
* @returns address of the target keypair
*/
export const addressFromMnemonic = (mnemonicOrUri: string, type: KeypairType = "sr25519") => {
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
}
21 changes: 21 additions & 0 deletions apps/extension/src/@talisman/util/isValidDerivationPath.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
75 changes: 68 additions & 7 deletions apps/extension/src/core/domains/accounts/handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
formatSuri,
getNextDerivationPathForMnemonic,
isValidAnyAddress,
sortAccounts,
Expand All @@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -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,
Expand All @@ -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<string> {
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
Expand Down Expand Up @@ -515,6 +533,43 @@ export default class AccountsHandler extends ExtensionHandler {
return true
}

private async addressLookup(lookup: RequestAddressLookup): Promise<string> {
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<string> {
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<TMessageType extends MessageTypes>(
id: string,
type: TMessageType,
Expand Down Expand Up @@ -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}`)
}
Expand Down
13 changes: 8 additions & 5 deletions apps/extension/src/core/domains/accounts/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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("")
}
Expand All @@ -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)
Expand Down Expand Up @@ -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}`
27 changes: 27 additions & 0 deletions apps/extension/src/core/domains/accounts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,12 @@ export interface RequestAccountRename {
export type RequestAccountCreateOptions =
| {
mnemonicId: string
derivationPath?: string
}
| {
mnemonic: string
confirmed: boolean
derivationPath?: string
}

export type RequestAccountCreate = {
Expand All @@ -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]
Expand All @@ -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]
}
6 changes: 0 additions & 6 deletions apps/extension/src/core/domains/mnemonics/handler.ts
Original file line number Diff line number Diff line change
@@ -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<TMessageType extends MessageTypes>(
Expand All @@ -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)
Expand Down
8 changes: 0 additions & 8 deletions apps/extension/src/core/domains/mnemonics/types.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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]
}
1 change: 1 addition & 0 deletions apps/extension/src/core/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
7 changes: 5 additions & 2 deletions apps/extension/src/ui/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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 }) =>
Expand Down
Loading