Allow keyringPair to create using a mnemonic wordlist (#1974)

This commit is contained in:
Valentin Fernandez
2025-02-05 06:41:14 -05:00
committed by GitHub
parent 8456870148
commit e25bb48ba9
5 changed files with 65 additions and 12 deletions
+33 -1
View File
@@ -6,7 +6,8 @@
import type { KeyringPair$Json } from './types.js';
import { hexToU8a, stringToU8a } from '@polkadot/util';
import { base64Decode, cryptoWaitReady, encodeAddress, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
import { base64Decode, cryptoWaitReady, encodeAddress, mnemonicGenerate, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
import * as languages from '@polkadot/util-crypto/mnemonic/wordlists/index';
import { decodePair } from './pair/decode.js';
import Keyring from './index.js';
@@ -574,4 +575,35 @@ describe('keypair', (): void => {
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('wordlist', (): void => {
it('creates keypair from different wordlists mnemonics', (): void => {
Object.keys(languages).forEach((language) => {
const mnemonic = mnemonicGenerate(12, languages[language as keyof typeof languages]);
const keyring = new Keyring({
type: 'ed25519'
});
expect(keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages[language as keyof typeof languages]
)).toBeDefined();
});
});
it('cannot create from invalid wordlist', (): void => {
const mnemonic = mnemonicGenerate(12, languages.japanese);
const keyring = new Keyring({
type: 'ed25519'
});
expect(() => keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages.english
)).toThrow('Invalid bip39 mnemonic specified');
});
});
});
+6 -6
View File
@@ -121,8 +121,8 @@ export class Keyring implements KeyringInstance {
* of an account backup), and then generates a keyring pair from it that it passes to
* `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.
*/
public addFromMnemonic (mnemonic: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
return this.addFromUri(mnemonic, meta, type);
public addFromMnemonic (mnemonic: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
return this.addFromUri(mnemonic, meta, type, wordlist);
}
/**
@@ -153,9 +153,9 @@ export class Keyring implements KeyringInstance {
* @summary Creates an account via an suri
* @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)
*/
public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
return this.addPair(
this.createFromUri(suri, meta, type)
this.createFromUri(suri, meta, type, wordlist)
);
}
@@ -203,7 +203,7 @@ export class Keyring implements KeyringInstance {
* @summary Creates a Keypair from an suri
* @description This creates a pair from the suri, but does not add it to the keyring
*/
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
// here we only aut-add the dev phrase if we have a hard-derived path
const suri = _suri.startsWith('//')
? `${DEV_PHRASE}${_suri}`
@@ -220,7 +220,7 @@ export class Keyring implements KeyringInstance {
if ([12, 15, 18, 21, 24].includes(parts.length)) {
seed = type === 'ethereum'
? mnemonicToLegacySeed(phrase, '', false, 64)
: mnemonicToMiniSecret(phrase, password);
: mnemonicToMiniSecret(phrase, password, wordlist);
} else {
if (phrase.length > 32) {
throw new Error('specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes');
+3 -3
View File
@@ -116,13 +116,13 @@ export interface KeyringInstance {
addPair (pair: KeyringPair): KeyringPair;
addFromAddress (address: string | Uint8Array, meta?: KeyringPair$Meta, encoded?: Uint8Array | null, type?: KeypairType, ignoreChecksum?: boolean): KeyringPair;
addFromJson (pair: KeyringPair$Json, ignoreChecksum?: boolean): KeyringPair;
addFromMnemonic (mnemonic: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
addFromMnemonic (mnemonic: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair;
addFromPair (pair: Keypair, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair
addFromSeed (seed: Uint8Array, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
addFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
addFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair;
createFromJson (json: KeyringPair$Json, ignoreChecksum?: boolean): KeyringPair;
createFromPair (pair: Keypair, meta: KeyringPair$Meta, type: KeypairType): KeyringPair
createFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair;
createFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair;
getPair (address: string | Uint8Array): KeyringPair;
getPairs (): KeyringPair[];
getPublicKeys (): Uint8Array[];
@@ -126,4 +126,22 @@ describe('keyExtractSuri', (): void => {
expect(test.path[0].isHard).toEqual(true);
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]));
});
it('derives on uncommon characters', (): void => {
const languageMnemonics = {
chineseSimplified: '熙 礼 淀 谋 耗 搜 雨 瑞 雷 合 析 感',
chineseTraditional: '召 胸 捕 乏 講 祥 隙 幫 動 框 場 給',
french: 'ruiner minute maison ouragan palourde piscine nerveux descente romance édifier ancien médaille',
japanese: 'ほったん はちみつ おやゆび ほかん いりぐち さんいん てぶくろ だいじょうぶ ふとん でぬかえ ちしき あわてる',
korean: '김밥 방향 논리 저절로 증상 지진 회장 오히려 시리즈 최근 학용품 곡식'
};
Object.keys(languageMnemonics).forEach((mnemonic) => {
const test = keyExtractSuri(mnemonic);
expect(test.password).not.toBeDefined();
expect(test.phrase).toEqual(mnemonic);
expect(test.path.length).toEqual(0);
});
});
});
+5 -2
View File
@@ -12,14 +12,17 @@ export interface ExtractResult {
phrase: string;
}
const RE_CAPTURE = /^(\w+( \w+)*)((\/\/?[^/]+)*)(\/\/\/(.*))?$/;
const RE_CAPTURE = /^((0x[a-fA-F0-9]+|[\p{L}\d]+(?: [\p{L}\d]+)*))((\/\/?[^/]+)*)(\/\/\/(.*))?$/u;
/**
* @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).
*/
export function keyExtractSuri (suri: string): ExtractResult {
// Normalize Unicode to NFC to avoid accent-related mismatches
const normalizedSuri = suri.normalize('NFC');
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
const matches = suri.match(RE_CAPTURE);
const matches = normalizedSuri.match(RE_CAPTURE);
if (matches === null) {
throw new Error('Unable to match provided value to a secret URI');