proto-signing: Add support for multiple accounts to DirectSecp256k1HdWallet
This commit is contained in:
parent
6a18c8e657
commit
5a15b108e6
@ -23,12 +23,13 @@ describe("DirectSecp256k1HdWallet", () => {
|
||||
it("works with options", async () => {
|
||||
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic, {
|
||||
bip39Password: "password123",
|
||||
hdPath: makeCosmoshubPath(123),
|
||||
hdPaths: [makeCosmoshubPath(123)],
|
||||
prefix: "yolo",
|
||||
});
|
||||
expect(wallet.mnemonic).toEqual(defaultMnemonic);
|
||||
expect((wallet as any).pubkey).not.toEqual(defaultPubkey);
|
||||
expect((wallet as any).address.slice(0, 4)).toEqual("yolo");
|
||||
const [{ pubkey, address }] = await wallet.getAccounts();
|
||||
expect(pubkey).not.toEqual(defaultPubkey);
|
||||
expect(address.slice(0, 4)).toEqual("yolo");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
HdPath,
|
||||
Random,
|
||||
Secp256k1,
|
||||
Secp256k1Keypair,
|
||||
sha256,
|
||||
Slip10,
|
||||
Slip10Curve,
|
||||
@ -15,6 +16,10 @@ import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx";
|
||||
import { AccountData, DirectSignResponse, OfflineDirectSigner } from "./signer";
|
||||
import { makeSignBytes } from "./signing";
|
||||
|
||||
interface AccountDataWithPrivkey extends AccountData {
|
||||
readonly privkey: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derivation information required to derive a keypair and an address from a mnemonic.
|
||||
*/
|
||||
@ -26,15 +31,19 @@ interface Secp256k1Derivation {
|
||||
export interface DirectSecp256k1HdWalletOptions {
|
||||
/** The password to use when deriving a BIP39 seed from a mnemonic. */
|
||||
readonly bip39Password: string;
|
||||
/** The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. */
|
||||
readonly hdPath: HdPath;
|
||||
/** The BIP-32/SLIP-10 derivation paths. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. */
|
||||
readonly hdPaths: readonly HdPath[];
|
||||
/** The bech32 address prefix (human readable part). Defaults to "cosmos". */
|
||||
readonly prefix: string;
|
||||
}
|
||||
|
||||
interface DirectSecp256k1HdWalletConstructorOptions extends Partial<DirectSecp256k1HdWalletOptions> {
|
||||
readonly seed: Uint8Array;
|
||||
}
|
||||
|
||||
const defaultOptions: DirectSecp256k1HdWalletOptions = {
|
||||
bip39Password: "",
|
||||
hdPath: makeCosmoshubPath(0),
|
||||
hdPaths: [makeCosmoshubPath(0)],
|
||||
prefix: "cosmos",
|
||||
};
|
||||
|
||||
@ -44,103 +53,105 @@ export class DirectSecp256k1HdWallet implements OfflineDirectSigner {
|
||||
* Restores a wallet from the given BIP39 mnemonic.
|
||||
*
|
||||
* @param mnemonic Any valid English mnemonic.
|
||||
* @param options An optional `DirectSecp256k1HdWalletOptions` object optionally containing a bip39Password, hdPath, and prefix.
|
||||
* @param options An optional `DirectSecp256k1HdWalletOptions` object optionally containing a bip39Password, hdPaths, and prefix.
|
||||
*/
|
||||
public static async fromMnemonic(
|
||||
mnemonic: string,
|
||||
options: Partial<DirectSecp256k1HdWalletOptions> = {},
|
||||
): Promise<DirectSecp256k1HdWallet> {
|
||||
const { bip39Password, hdPath, prefix } = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
const mnemonicChecked = new EnglishMnemonic(mnemonic);
|
||||
const seed = await Bip39.mnemonicToSeed(mnemonicChecked, bip39Password);
|
||||
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
|
||||
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
|
||||
return new DirectSecp256k1HdWallet(
|
||||
mnemonicChecked,
|
||||
hdPath,
|
||||
privkey,
|
||||
Secp256k1.compressPubkey(uncompressed),
|
||||
prefix,
|
||||
);
|
||||
const seed = await Bip39.mnemonicToSeed(mnemonicChecked, options.bip39Password);
|
||||
return new DirectSecp256k1HdWallet(mnemonicChecked, {
|
||||
...options,
|
||||
seed: seed,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`.
|
||||
* @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos".
|
||||
* @param options An optional `DirectSecp256k1HdWalletOptions` object optionally containing a bip39Password, hdPaths, and prefix.
|
||||
*/
|
||||
public static async generate(
|
||||
length: 12 | 15 | 18 | 21 | 24 = 12,
|
||||
hdPath: HdPath = makeCosmoshubPath(0),
|
||||
prefix = "cosmos",
|
||||
options: Partial<DirectSecp256k1HdWalletOptions> = {},
|
||||
): Promise<DirectSecp256k1HdWallet> {
|
||||
const entropyLength = 4 * Math.floor((11 * length) / 33);
|
||||
const entropy = Random.getBytes(entropyLength);
|
||||
const mnemonic = Bip39.encode(entropy);
|
||||
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic.toString(), { hdPath: hdPath, prefix: prefix });
|
||||
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic.toString(), options);
|
||||
}
|
||||
|
||||
/** Base secret */
|
||||
private readonly secret: EnglishMnemonic;
|
||||
/** Derivation instruction */
|
||||
/** BIP39 seed */
|
||||
private readonly seed: Uint8Array;
|
||||
/** Derivation instructions */
|
||||
private readonly accounts: readonly Secp256k1Derivation[];
|
||||
/** Derived data */
|
||||
private readonly pubkey: Uint8Array;
|
||||
private readonly privkey: Uint8Array;
|
||||
|
||||
protected constructor(
|
||||
mnemonic: EnglishMnemonic,
|
||||
hdPath: HdPath,
|
||||
privkey: Uint8Array,
|
||||
pubkey: Uint8Array,
|
||||
prefix: string,
|
||||
) {
|
||||
protected constructor(mnemonic: EnglishMnemonic, options: DirectSecp256k1HdWalletConstructorOptions) {
|
||||
const { seed, hdPaths, prefix } = { ...defaultOptions, ...options };
|
||||
this.secret = mnemonic;
|
||||
this.accounts = [
|
||||
{
|
||||
hdPath: hdPath,
|
||||
prefix: prefix,
|
||||
},
|
||||
];
|
||||
this.privkey = privkey;
|
||||
this.pubkey = pubkey;
|
||||
this.seed = seed;
|
||||
this.accounts = hdPaths.map((hdPath) => ({
|
||||
hdPath: hdPath,
|
||||
prefix: prefix,
|
||||
}));
|
||||
}
|
||||
|
||||
public get mnemonic(): string {
|
||||
return this.secret.toString();
|
||||
}
|
||||
|
||||
private get address(): string {
|
||||
return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey));
|
||||
}
|
||||
|
||||
public async getAccounts(): Promise<readonly AccountData[]> {
|
||||
return [
|
||||
{
|
||||
algo: "secp256k1",
|
||||
address: this.address,
|
||||
pubkey: this.pubkey,
|
||||
},
|
||||
];
|
||||
const accountsWithPrivkeys = await this.getAccountsWithPrivkeys();
|
||||
return accountsWithPrivkeys.map(({ algo, pubkey, address }) => ({
|
||||
algo: algo,
|
||||
pubkey: pubkey,
|
||||
address: address,
|
||||
}));
|
||||
}
|
||||
|
||||
public async signDirect(address: string, signDoc: SignDoc): Promise<DirectSignResponse> {
|
||||
const signBytes = makeSignBytes(signDoc);
|
||||
if (address !== this.address) {
|
||||
throw new Error(`Address ${address} not found in wallet`);
|
||||
public async signDirect(signerAddress: string, signDoc: SignDoc): Promise<DirectSignResponse> {
|
||||
const accounts = await this.getAccountsWithPrivkeys();
|
||||
const account = accounts.find(({ address }) => address === signerAddress);
|
||||
if (account === undefined) {
|
||||
throw new Error(`Address ${signerAddress} not found in wallet`);
|
||||
}
|
||||
const { privkey, pubkey } = account;
|
||||
const signBytes = makeSignBytes(signDoc);
|
||||
const hashedMessage = sha256(signBytes);
|
||||
const signature = await Secp256k1.createSignature(hashedMessage, this.privkey);
|
||||
const signature = await Secp256k1.createSignature(hashedMessage, privkey);
|
||||
const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]);
|
||||
const stdSignature = encodeSecp256k1Signature(this.pubkey, signatureBytes);
|
||||
const stdSignature = encodeSecp256k1Signature(pubkey, signatureBytes);
|
||||
return {
|
||||
signed: signDoc,
|
||||
signature: stdSignature,
|
||||
};
|
||||
}
|
||||
|
||||
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…
Reference in New Issue
Block a user