mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-10 21:49:15 +00:00
amino: Add support for multiple accounts to Secp256k1HdWallet
This commit is contained in:
parent
5a15b108e6
commit
5c39bc9a5a
@ -25,12 +25,13 @@ describe("Secp256k1HdWallet", () => {
|
|||||||
it("works with options", async () => {
|
it("works with options", async () => {
|
||||||
const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic, {
|
const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic, {
|
||||||
bip39Password: "password123",
|
bip39Password: "password123",
|
||||||
hdPath: makeCosmoshubPath(123),
|
hdPaths: [makeCosmoshubPath(123)],
|
||||||
prefix: "yolo",
|
prefix: "yolo",
|
||||||
});
|
});
|
||||||
expect(wallet.mnemonic).toEqual(defaultMnemonic);
|
expect(wallet.mnemonic).toEqual(defaultMnemonic);
|
||||||
expect((wallet as any).pubkey).not.toEqual(defaultPubkey);
|
const [account] = await wallet.getAccounts();
|
||||||
expect((wallet as any).address.slice(0, 4)).toEqual("yolo");
|
expect(account.pubkey).not.toEqual(defaultPubkey);
|
||||||
|
expect(account.address.slice(0, 4)).toEqual("yolo");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
pathToString,
|
pathToString,
|
||||||
Random,
|
Random,
|
||||||
Secp256k1,
|
Secp256k1,
|
||||||
|
Secp256k1Keypair,
|
||||||
sha256,
|
sha256,
|
||||||
Slip10,
|
Slip10,
|
||||||
Slip10Curve,
|
Slip10Curve,
|
||||||
@ -27,6 +28,10 @@ import {
|
|||||||
supportedAlgorithms,
|
supportedAlgorithms,
|
||||||
} from "./wallet";
|
} from "./wallet";
|
||||||
|
|
||||||
|
interface AccountDataWithPrivkey extends AccountData {
|
||||||
|
readonly privkey: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
const serializationTypeV1 = "secp256k1wallet-v1";
|
const serializationTypeV1 = "secp256k1wallet-v1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,15 +115,19 @@ interface DerivationInfo {
|
|||||||
export interface Secp256k1HdWalletOptions {
|
export interface Secp256k1HdWalletOptions {
|
||||||
/** The password to use when deriving a BIP39 seed from a mnemonic. */
|
/** The password to use when deriving a BIP39 seed from a mnemonic. */
|
||||||
readonly bip39Password: string;
|
readonly bip39Password: string;
|
||||||
/** The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. */
|
/** The BIP-32/SLIP-10 derivation paths. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. */
|
||||||
readonly hdPath: HdPath;
|
readonly hdPaths: readonly HdPath[];
|
||||||
/** The bech32 address prefix (human readable part). Defaults to "cosmos". */
|
/** The bech32 address prefix (human readable part). Defaults to "cosmos". */
|
||||||
readonly prefix: string;
|
readonly prefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Secp256k1HdWalletConstructorOptions extends Partial<Secp256k1HdWalletOptions> {
|
||||||
|
readonly seed: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultOptions: Secp256k1HdWalletOptions = {
|
const defaultOptions: Secp256k1HdWalletOptions = {
|
||||||
bip39Password: "",
|
bip39Password: "",
|
||||||
hdPath: makeCosmoshubPath(0),
|
hdPaths: [makeCosmoshubPath(0)],
|
||||||
prefix: "cosmos",
|
prefix: "cosmos",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,45 +136,34 @@ export class Secp256k1HdWallet implements OfflineAminoSigner {
|
|||||||
* Restores a wallet from the given BIP39 mnemonic.
|
* Restores a wallet from the given BIP39 mnemonic.
|
||||||
*
|
*
|
||||||
* @param mnemonic Any valid English mnemonic.
|
* @param mnemonic Any valid English mnemonic.
|
||||||
* @param options An optional `Secp256k1HdWalletOptions` object optionally containing a bip39Password, hdPath, and prefix.
|
* @param options An optional `Secp256k1HdWalletOptions` object optionally containing a bip39Password, hdPaths, and prefix.
|
||||||
*/
|
*/
|
||||||
public static async fromMnemonic(
|
public static async fromMnemonic(
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
options: Partial<Secp256k1HdWalletOptions> = {},
|
options: Partial<Secp256k1HdWalletOptions> = {},
|
||||||
): Promise<Secp256k1HdWallet> {
|
): Promise<Secp256k1HdWallet> {
|
||||||
const { bip39Password, hdPath, prefix } = {
|
|
||||||
...defaultOptions,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
const mnemonicChecked = new EnglishMnemonic(mnemonic);
|
const mnemonicChecked = new EnglishMnemonic(mnemonic);
|
||||||
const seed = await Bip39.mnemonicToSeed(mnemonicChecked, bip39Password);
|
const seed = await Bip39.mnemonicToSeed(mnemonicChecked, options.bip39Password);
|
||||||
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
|
return new Secp256k1HdWallet(mnemonicChecked, {
|
||||||
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
|
...options,
|
||||||
return new Secp256k1HdWallet(
|
seed: seed,
|
||||||
mnemonicChecked,
|
});
|
||||||
hdPath,
|
|
||||||
privkey,
|
|
||||||
Secp256k1.compressPubkey(uncompressed),
|
|
||||||
prefix,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new wallet with a BIP39 mnemonic of the given length.
|
* Generates a new wallet with a BIP39 mnemonic of the given length.
|
||||||
*
|
*
|
||||||
* @param length The number of words in the mnemonic (12, 15, 18, 21 or 24).
|
* @param length The number of words in the mnemonic (12, 15, 18, 21 or 24).
|
||||||
* @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`.
|
* @param options An optional `Secp256k1HdWalletOptions` object optionally containing a bip39Password, hdPaths, and prefix.
|
||||||
* @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos".
|
|
||||||
*/
|
*/
|
||||||
public static async generate(
|
public static async generate(
|
||||||
length: 12 | 15 | 18 | 21 | 24 = 12,
|
length: 12 | 15 | 18 | 21 | 24 = 12,
|
||||||
hdPath: HdPath = makeCosmoshubPath(0),
|
options: Partial<Secp256k1HdWalletOptions> = {},
|
||||||
prefix = "cosmos",
|
|
||||||
): Promise<Secp256k1HdWallet> {
|
): Promise<Secp256k1HdWallet> {
|
||||||
const entropyLength = 4 * Math.floor((11 * length) / 33);
|
const entropyLength = 4 * Math.floor((11 * length) / 33);
|
||||||
const entropy = Random.getBytes(entropyLength);
|
const entropy = Random.getBytes(entropyLength);
|
||||||
const mnemonic = Bip39.encode(entropy);
|
const mnemonic = Bip39.encode(entropy);
|
||||||
return Secp256k1HdWallet.fromMnemonic(mnemonic.toString(), { hdPath: hdPath, prefix: prefix });
|
return Secp256k1HdWallet.fromMnemonic(mnemonic.toString(), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,12 +210,17 @@ export class Secp256k1HdWallet implements OfflineAminoSigner {
|
|||||||
const { mnemonic, accounts } = decryptedDocument;
|
const { mnemonic, accounts } = decryptedDocument;
|
||||||
assert(typeof mnemonic === "string");
|
assert(typeof mnemonic === "string");
|
||||||
if (!Array.isArray(accounts)) throw new Error("Property 'accounts' is not an array");
|
if (!Array.isArray(accounts)) throw new Error("Property 'accounts' is not an array");
|
||||||
if (accounts.length !== 1) throw new Error("Property 'accounts' only supports one entry");
|
if (!accounts.every((account) => isDerivationJson(account))) {
|
||||||
const account = accounts[0];
|
throw new Error("Account is not in the correct format.");
|
||||||
if (!isDerivationJson(account)) throw new Error("Account is not in the correct format.");
|
}
|
||||||
|
const firstPrefix = accounts[0].prefix;
|
||||||
|
if (!accounts.every(({ prefix }) => prefix === firstPrefix)) {
|
||||||
|
throw new Error("Accounts do not all have the same prefix");
|
||||||
|
}
|
||||||
|
const hdPaths = accounts.map(({ hdPath }) => stringToPath(hdPath));
|
||||||
return Secp256k1HdWallet.fromMnemonic(mnemonic, {
|
return Secp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||||
hdPath: stringToPath(account.hdPath),
|
hdPaths: hdPaths,
|
||||||
prefix: account.prefix,
|
prefix: firstPrefix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -237,58 +240,47 @@ export class Secp256k1HdWallet implements OfflineAminoSigner {
|
|||||||
|
|
||||||
/** Base secret */
|
/** Base secret */
|
||||||
private readonly secret: EnglishMnemonic;
|
private readonly secret: EnglishMnemonic;
|
||||||
|
/** BIP39 seed */
|
||||||
|
private readonly seed: Uint8Array;
|
||||||
/** Derivation instruction */
|
/** Derivation instruction */
|
||||||
private readonly accounts: readonly DerivationInfo[];
|
private readonly accounts: readonly DerivationInfo[];
|
||||||
/** Derived data */
|
|
||||||
private readonly pubkey: Uint8Array;
|
|
||||||
private readonly privkey: Uint8Array;
|
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(mnemonic: EnglishMnemonic, options: Secp256k1HdWalletConstructorOptions) {
|
||||||
mnemonic: EnglishMnemonic,
|
const { seed, hdPaths, prefix } = { ...defaultOptions, ...options };
|
||||||
hdPath: HdPath,
|
|
||||||
privkey: Uint8Array,
|
|
||||||
pubkey: Uint8Array,
|
|
||||||
prefix: string,
|
|
||||||
) {
|
|
||||||
this.secret = mnemonic;
|
this.secret = mnemonic;
|
||||||
this.accounts = [
|
this.seed = seed;
|
||||||
{
|
this.accounts = hdPaths.map((hdPath) => ({
|
||||||
hdPath: hdPath,
|
hdPath: hdPath,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
},
|
}));
|
||||||
];
|
|
||||||
this.privkey = privkey;
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get mnemonic(): string {
|
public get mnemonic(): string {
|
||||||
return this.secret.toString();
|
return this.secret.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get address(): string {
|
|
||||||
return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAccounts(): Promise<readonly AccountData[]> {
|
public async getAccounts(): Promise<readonly AccountData[]> {
|
||||||
return [
|
const accountsWithPrivkeys = await this.getAccountsWithPrivkeys();
|
||||||
{
|
return accountsWithPrivkeys.map(({ algo, pubkey, address }) => ({
|
||||||
algo: "secp256k1",
|
algo: algo,
|
||||||
address: this.address,
|
pubkey: pubkey,
|
||||||
pubkey: this.pubkey,
|
address: address,
|
||||||
},
|
}));
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
|
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
|
||||||
if (signerAddress !== this.address) {
|
const accounts = await this.getAccountsWithPrivkeys();
|
||||||
|
const account = accounts.find(({ address }) => address === signerAddress);
|
||||||
|
if (account === undefined) {
|
||||||
throw new Error(`Address ${signerAddress} not found in wallet`);
|
throw new Error(`Address ${signerAddress} not found in wallet`);
|
||||||
}
|
}
|
||||||
|
const { privkey, pubkey } = account;
|
||||||
const message = sha256(serializeSignDoc(signDoc));
|
const message = sha256(serializeSignDoc(signDoc));
|
||||||
const signature = await Secp256k1.createSignature(message, this.privkey);
|
const signature = await Secp256k1.createSignature(message, privkey);
|
||||||
const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]);
|
const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]);
|
||||||
return {
|
return {
|
||||||
signed: signDoc,
|
signed: signDoc,
|
||||||
signature: encodeSecp256k1Signature(this.pubkey, signatureBytes),
|
signature: encodeSecp256k1Signature(pubkey, signatureBytes),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,4 +333,28 @@ export class Secp256k1HdWallet implements OfflineAminoSigner {
|
|||||||
};
|
};
|
||||||
return JSON.stringify(out);
|
return JSON.stringify(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getKeyPair(hdPath: HdPath): Promise<Secp256k1Keypair> {
|
||||||
|
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, this.seed, hdPath);
|
||||||
|
const { pubkey } = await Secp256k1.makeKeypair(privkey);
|
||||||
|
return {
|
||||||
|
privkey: privkey,
|
||||||
|
pubkey: Secp256k1.compressPubkey(pubkey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAccountsWithPrivkeys(): Promise<readonly AccountDataWithPrivkey[]> {
|
||||||
|
return Promise.all(
|
||||||
|
this.accounts.map(async ({ hdPath, prefix }) => {
|
||||||
|
const { privkey, pubkey } = await this.getKeyPair(hdPath);
|
||||||
|
const address = Bech32.encode(prefix, rawSecp256k1PubkeyToRawAddress(pubkey));
|
||||||
|
return {
|
||||||
|
algo: "secp256k1" as const,
|
||||||
|
privkey: privkey,
|
||||||
|
pubkey: pubkey,
|
||||||
|
address: address,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user