Skip to content

Commit e25bb48

Browse files
Allow keyringPair to create using a mnemonic wordlist (#1974)
1 parent 8456870 commit e25bb48

File tree

5 files changed

+65
-12
lines changed

5 files changed

+65
-12
lines changed

packages/keyring/src/index.spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import type { KeyringPair$Json } from './types.js';
77

88
import { hexToU8a, stringToU8a } from '@polkadot/util';
9-
import { base64Decode, cryptoWaitReady, encodeAddress, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
9+
import { base64Decode, cryptoWaitReady, encodeAddress, mnemonicGenerate, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
10+
import * as languages from '@polkadot/util-crypto/mnemonic/wordlists/index';
1011

1112
import { decodePair } from './pair/decode.js';
1213
import Keyring from './index.js';
@@ -574,4 +575,35 @@ describe('keypair', (): void => {
574575
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
575576
});
576577
});
578+
579+
describe('wordlist', (): void => {
580+
it('creates keypair from different wordlists mnemonics', (): void => {
581+
Object.keys(languages).forEach((language) => {
582+
const mnemonic = mnemonicGenerate(12, languages[language as keyof typeof languages]);
583+
const keyring = new Keyring({
584+
type: 'ed25519'
585+
});
586+
587+
expect(keyring.addFromMnemonic(
588+
mnemonic,
589+
{},
590+
'ed25519',
591+
languages[language as keyof typeof languages]
592+
)).toBeDefined();
593+
});
594+
});
595+
it('cannot create from invalid wordlist', (): void => {
596+
const mnemonic = mnemonicGenerate(12, languages.japanese);
597+
const keyring = new Keyring({
598+
type: 'ed25519'
599+
});
600+
601+
expect(() => keyring.addFromMnemonic(
602+
mnemonic,
603+
{},
604+
'ed25519',
605+
languages.english
606+
)).toThrow('Invalid bip39 mnemonic specified');
607+
});
608+
});
577609
});

packages/keyring/src/keyring.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ export class Keyring implements KeyringInstance {
121121
* of an account backup), and then generates a keyring pair from it that it passes to
122122
* `addPair` to stores in a keyring pair dictionary the public key of the generated pair as a key and the pair as the associated value.
123123
*/
124-
public addFromMnemonic (mnemonic: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
125-
return this.addFromUri(mnemonic, meta, type);
124+
public addFromMnemonic (mnemonic: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
125+
return this.addFromUri(mnemonic, meta, type, wordlist);
126126
}
127127

128128
/**
@@ -153,9 +153,9 @@ export class Keyring implements KeyringInstance {
153153
* @summary Creates an account via an suri
154154
* @description Extracts the phrase, path and password from a SURI format for specifying secret keys `<secret>/<soft-key>//<hard-key>///<password>` (the `///password` may be omitted, and `/<soft-key>` and `//<hard-key>` maybe repeated and mixed). The secret can be a hex string, mnemonic phrase or a string (to be padded)
155155
*/
156-
public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
156+
public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
157157
return this.addPair(
158-
this.createFromUri(suri, meta, type)
158+
this.createFromUri(suri, meta, type, wordlist)
159159
);
160160
}
161161

@@ -203,7 +203,7 @@ export class Keyring implements KeyringInstance {
203203
* @summary Creates a Keypair from an suri
204204
* @description This creates a pair from the suri, but does not add it to the keyring
205205
*/
206-
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
206+
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
207207
// here we only aut-add the dev phrase if we have a hard-derived path
208208
const suri = _suri.startsWith('//')
209209
? `${DEV_PHRASE}${_suri}`
@@ -220,7 +220,7 @@ export class Keyring implements KeyringInstance {
220220
if ([12, 15, 18, 21, 24].includes(parts.length)) {
221221
seed = type === 'ethereum'
222222
? mnemonicToLegacySeed(phrase, '', false, 64)
223-
: mnemonicToMiniSecret(phrase, password);
223+
: mnemonicToMiniSecret(phrase, password, wordlist);
224224
} else {
225225
if (phrase.length > 32) {
226226
throw new Error('specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes');

packages/keyring/src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,13 @@ export interface KeyringInstance {
116116
addPair (pair: KeyringPair): KeyringPair;
117117
addFromAddress (address: string | Uint8Array, meta?: KeyringPair$Meta, encoded?: Uint8Array | null, type?: KeypairType, ignoreChecksum?: boolean): KeyringPair;
118118
addFromJson (pair: KeyringPair$Json, ignoreChecksum?: boolean): KeyringPair;
119-
addFromMnemonic (mnemonic: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
119+
addFromMnemonic (mnemonic: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair;
120120
addFromPair (pair: Keypair, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair
121121
addFromSeed (seed: Uint8Array, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
122-
addFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
122+
addFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair;
123123
createFromJson (json: KeyringPair$Json, ignoreChecksum?: boolean): KeyringPair;
124124
createFromPair (pair: Keypair, meta: KeyringPair$Meta, type: KeypairType): KeyringPair
125-
createFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
125+
createFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair;
126126
getPair (address: string | Uint8Array): KeyringPair;
127127
getPairs (): KeyringPair[];
128128
getPublicKeys (): Uint8Array[];

packages/util-crypto/src/key/extractSuri.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,22 @@ describe('keyExtractSuri', (): void => {
126126
expect(test.path[0].isHard).toEqual(true);
127127
expect(test.path[0].chainCode).toEqual(Uint8Array.from([20, 65, 108, 105, 99, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
128128
});
129+
130+
it('derives on uncommon characters', (): void => {
131+
const languageMnemonics = {
132+
chineseSimplified: '熙 礼 淀 谋 耗 搜 雨 瑞 雷 合 析 感',
133+
chineseTraditional: '召 胸 捕 乏 講 祥 隙 幫 動 框 場 給',
134+
french: 'ruiner minute maison ouragan palourde piscine nerveux descente romance édifier ancien médaille',
135+
japanese: 'ほったん はちみつ おやゆび ほかん いりぐち さんいん てぶくろ だいじょうぶ ふとん でぬかえ ちしき あわてる',
136+
korean: '김밥 방향 논리 저절로 증상 지진 회장 오히려 시리즈 최근 학용품 곡식'
137+
};
138+
139+
Object.keys(languageMnemonics).forEach((mnemonic) => {
140+
const test = keyExtractSuri(mnemonic);
141+
142+
expect(test.password).not.toBeDefined();
143+
expect(test.phrase).toEqual(mnemonic);
144+
expect(test.path.length).toEqual(0);
145+
});
146+
});
129147
});

packages/util-crypto/src/key/extractSuri.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ export interface ExtractResult {
1212
phrase: string;
1313
}
1414

15-
const RE_CAPTURE = /^(\w+( \w+)*)((\/\/?[^/]+)*)(\/\/\/(.*))?$/;
15+
const RE_CAPTURE = /^((0x[a-fA-F0-9]+|[\p{L}\d]+(?: [\p{L}\d]+)*))((\/\/?[^/]+)*)(\/\/\/(.*))?$/u;
1616

1717
/**
1818
* @description Extracts the phrase, path and password from a SURI format for specifying secret keys `<secret>/<soft-key>//<hard-key>///<password>` (the `///password` may be omitted, and `/<soft-key>` and `//<hard-key>` maybe repeated and mixed).
1919
*/
2020
export function keyExtractSuri (suri: string): ExtractResult {
21+
// Normalize Unicode to NFC to avoid accent-related mismatches
22+
const normalizedSuri = suri.normalize('NFC');
23+
2124
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
22-
const matches = suri.match(RE_CAPTURE);
25+
const matches = normalizedSuri.match(RE_CAPTURE);
2326

2427
if (matches === null) {
2528
throw new Error('Unable to match provided value to a secret URI');

0 commit comments

Comments
 (0)