mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-11 14:09:15 +00:00
Create DirectSecp256k1Wallet
This commit is contained in:
parent
d07ec8a77b
commit
db1f183247
@ -46,12 +46,12 @@
|
||||
"postdefine-proto": "prettier --write \"src/codec/generated/codecimpl.*\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/launchpad": "^0.22.3",
|
||||
"long": "^4.0.0",
|
||||
"protobufjs": "~6.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cosmjs/encoding": "^0.22.3",
|
||||
"@cosmjs/launchpad": "^0.22.3",
|
||||
"@cosmjs/utils": "^0.22.3"
|
||||
}
|
||||
}
|
||||
|
70
packages/proto-signing/src/directsecp256k1wallet.spec.ts
Normal file
70
packages/proto-signing/src/directsecp256k1wallet.spec.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto";
|
||||
import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding";
|
||||
|
||||
import { DirectSecp256k1Wallet } from "./directsecp256k1wallet";
|
||||
|
||||
describe("DirectSecp256k1Wallet", () => {
|
||||
// m/44'/118'/0'/0/0
|
||||
// pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6
|
||||
const defaultMnemonic = "special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling";
|
||||
const defaultPubkey = fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6");
|
||||
const defaultAddress = "cosmos1jhg0e7s6gn44tfc5k37kr04sznyhedtc9rzys5";
|
||||
|
||||
describe("fromMnemonic", () => {
|
||||
it("works", async () => {
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(defaultMnemonic);
|
||||
expect(wallet).toBeTruthy();
|
||||
expect(wallet.mnemonic).toEqual(defaultMnemonic);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generate", () => {
|
||||
it("defaults to 12 words", async () => {
|
||||
const wallet = await DirectSecp256k1Wallet.generate();
|
||||
expect(wallet.mnemonic.split(" ").length).toEqual(12);
|
||||
});
|
||||
|
||||
it("can use different mnemonic lengths", async () => {
|
||||
expect((await DirectSecp256k1Wallet.generate(12)).mnemonic.split(" ").length).toEqual(12);
|
||||
expect((await DirectSecp256k1Wallet.generate(15)).mnemonic.split(" ").length).toEqual(15);
|
||||
expect((await DirectSecp256k1Wallet.generate(18)).mnemonic.split(" ").length).toEqual(18);
|
||||
expect((await DirectSecp256k1Wallet.generate(21)).mnemonic.split(" ").length).toEqual(21);
|
||||
expect((await DirectSecp256k1Wallet.generate(24)).mnemonic.split(" ").length).toEqual(24);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccounts", () => {
|
||||
it("resolves to a list of accounts if enabled", async () => {
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(defaultMnemonic);
|
||||
const accounts = await wallet.getAccounts();
|
||||
expect(accounts.length).toEqual(1);
|
||||
expect(accounts[0]).toEqual({
|
||||
address: defaultAddress,
|
||||
algo: "secp256k1",
|
||||
pubkey: defaultPubkey,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates the same address as Go implementation", async () => {
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(
|
||||
"oyster design unusual machine spread century engine gravity focus cave carry slot",
|
||||
);
|
||||
const [{ address }] = await wallet.getAccounts();
|
||||
expect(address).toEqual("cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sign", () => {
|
||||
it("resolves to valid signature if enabled", async () => {
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(defaultMnemonic);
|
||||
const message = toAscii("foo bar");
|
||||
const signature = await wallet.sign(defaultAddress, message);
|
||||
const valid = await Secp256k1.verifySignature(
|
||||
Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)),
|
||||
new Sha256(message).digest(),
|
||||
defaultPubkey,
|
||||
);
|
||||
expect(valid).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
125
packages/proto-signing/src/directsecp256k1wallet.ts
Normal file
125
packages/proto-signing/src/directsecp256k1wallet.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import {
|
||||
Bip39,
|
||||
EnglishMnemonic,
|
||||
HdPath,
|
||||
Random,
|
||||
Secp256k1,
|
||||
Sha256,
|
||||
Slip10,
|
||||
Slip10Curve,
|
||||
} from "@cosmjs/crypto";
|
||||
import {
|
||||
AccountData,
|
||||
encodeSecp256k1Signature,
|
||||
makeCosmoshubPath,
|
||||
rawSecp256k1PubkeyToAddress,
|
||||
StdSignature,
|
||||
} from "@cosmjs/launchpad";
|
||||
|
||||
/**
|
||||
* Derivation information required to derive a keypair and an address from a mnemonic.
|
||||
*/
|
||||
interface Secp256k1Derivation {
|
||||
readonly hdPath: HdPath;
|
||||
readonly prefix: string;
|
||||
}
|
||||
|
||||
/** A wallet for protobuf based signing using SIGN_MODE_DIRECT */
|
||||
export class DirectSecp256k1Wallet {
|
||||
/**
|
||||
* Restores a wallet from the given BIP39 mnemonic.
|
||||
*
|
||||
* @param mnemonic Any valid English mnemonic.
|
||||
* @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".
|
||||
*/
|
||||
public static async fromMnemonic(
|
||||
mnemonic: string,
|
||||
hdPath: HdPath = makeCosmoshubPath(0),
|
||||
prefix = "cosmos",
|
||||
): Promise<DirectSecp256k1Wallet> {
|
||||
const mnemonicChecked = new EnglishMnemonic(mnemonic);
|
||||
const seed = await Bip39.mnemonicToSeed(mnemonicChecked);
|
||||
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
|
||||
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
|
||||
return new DirectSecp256k1Wallet(
|
||||
mnemonicChecked,
|
||||
hdPath,
|
||||
privkey,
|
||||
Secp256k1.compressPubkey(uncompressed),
|
||||
prefix,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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".
|
||||
*/
|
||||
public static async generate(
|
||||
length: 12 | 15 | 18 | 21 | 24 = 12,
|
||||
hdPath: HdPath = makeCosmoshubPath(0),
|
||||
prefix = "cosmos",
|
||||
): Promise<DirectSecp256k1Wallet> {
|
||||
const entropyLength = 4 * Math.floor((11 * length) / 33);
|
||||
const entropy = Random.getBytes(entropyLength);
|
||||
const mnemonic = Bip39.encode(entropy);
|
||||
return DirectSecp256k1Wallet.fromMnemonic(mnemonic.toString(), hdPath, prefix);
|
||||
}
|
||||
|
||||
/** Base secret */
|
||||
private readonly secret: EnglishMnemonic;
|
||||
/** Derivation instruction */
|
||||
private readonly accounts: readonly Secp256k1Derivation[];
|
||||
/** Derived data */
|
||||
private readonly pubkey: Uint8Array;
|
||||
private readonly privkey: Uint8Array;
|
||||
|
||||
private constructor(
|
||||
mnemonic: EnglishMnemonic,
|
||||
hdPath: HdPath,
|
||||
privkey: Uint8Array,
|
||||
pubkey: Uint8Array,
|
||||
prefix: string,
|
||||
) {
|
||||
this.secret = mnemonic;
|
||||
this.accounts = [
|
||||
{
|
||||
hdPath: hdPath,
|
||||
prefix: prefix,
|
||||
},
|
||||
];
|
||||
this.privkey = privkey;
|
||||
this.pubkey = pubkey;
|
||||
}
|
||||
|
||||
public get mnemonic(): string {
|
||||
return this.secret.toString();
|
||||
}
|
||||
|
||||
private get address(): string {
|
||||
return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix);
|
||||
}
|
||||
|
||||
public async getAccounts(): Promise<readonly AccountData[]> {
|
||||
return [
|
||||
{
|
||||
algo: "secp256k1",
|
||||
address: this.address,
|
||||
pubkey: this.pubkey,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public async sign(address: string, message: Uint8Array): Promise<StdSignature> {
|
||||
if (address !== this.address) {
|
||||
throw new Error(`Address ${address} not found in wallet`);
|
||||
}
|
||||
const hashedMessage = new Sha256(message).digest();
|
||||
const signature = await Secp256k1.createSignature(hashedMessage, this.privkey);
|
||||
const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]);
|
||||
return encodeSecp256k1Signature(this.pubkey, signatureBytes);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export { Coin } from "./msgs";
|
||||
export { cosmosField } from "./decorator";
|
||||
export { Registry } from "./registry";
|
||||
export { DirectSecp256k1Wallet } from "./directsecp256k1wallet";
|
||||
export { makeAuthInfo, makeSignBytes } from "./signing";
|
||||
|
@ -1,8 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Bech32, fromBase64, fromHex, toHex } from "@cosmjs/encoding";
|
||||
import { Secp256k1Wallet } from "@cosmjs/launchpad";
|
||||
|
||||
import { cosmos } from "./codec";
|
||||
import { DirectSecp256k1Wallet } from "./directsecp256k1wallet";
|
||||
import { defaultRegistry } from "./msgs";
|
||||
import { Registry, TxBodyValue } from "./registry";
|
||||
import { makeAuthInfo, makeSignBytes } from "./signing";
|
||||
@ -69,7 +69,7 @@ describe("signing", () => {
|
||||
const gasLimit = 200000;
|
||||
|
||||
it("correctly parses test vectors", async () => {
|
||||
const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const [{ address, pubkey: pubkeyBytes }] = await wallet.getAccounts();
|
||||
|
||||
testVectors.forEach(({ signedTxBytes }) => {
|
||||
@ -100,7 +100,7 @@ describe("signing", () => {
|
||||
|
||||
it("correctly generates test vectors", async () => {
|
||||
const myRegistry = new Registry();
|
||||
const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const [{ address, pubkey: pubkeyBytes }] = await wallet.getAccounts();
|
||||
const publicKey = PublicKey.create({
|
||||
secp256k1: pubkeyBytes,
|
||||
|
2
packages/proto-signing/src/testutils.spec.ts
Normal file
2
packages/proto-signing/src/testutils.spec.ts
Normal file
@ -0,0 +1,2 @@
|
||||
/** @see https://rgxdb.com/r/1NUN74O6 */
|
||||
export const base64Matcher = /^(?:[a-zA-Z0-9+/]{4})*(?:|(?:[a-zA-Z0-9+/]{3}=)|(?:[a-zA-Z0-9+/]{2}==)|(?:[a-zA-Z0-9+/]{1}===))$/;
|
37
packages/proto-signing/types/directsecp256k1wallet.d.ts
vendored
Normal file
37
packages/proto-signing/types/directsecp256k1wallet.d.ts
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
import { HdPath } from "@cosmjs/crypto";
|
||||
import { AccountData, StdSignature } from "@cosmjs/launchpad";
|
||||
/** A wallet for protobuf based signing using SIGN_MODE_DIRECT */
|
||||
export declare class DirectSecp256k1Wallet {
|
||||
/**
|
||||
* Restores a wallet from the given BIP39 mnemonic.
|
||||
*
|
||||
* @param mnemonic Any valid English mnemonic.
|
||||
* @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".
|
||||
*/
|
||||
static fromMnemonic(mnemonic: string, hdPath?: HdPath, prefix?: string): Promise<DirectSecp256k1Wallet>;
|
||||
/**
|
||||
* 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".
|
||||
*/
|
||||
static generate(
|
||||
length?: 12 | 15 | 18 | 21 | 24,
|
||||
hdPath?: HdPath,
|
||||
prefix?: string,
|
||||
): Promise<DirectSecp256k1Wallet>;
|
||||
/** Base secret */
|
||||
private readonly secret;
|
||||
/** Derivation instruction */
|
||||
private readonly accounts;
|
||||
/** Derived data */
|
||||
private readonly pubkey;
|
||||
private readonly privkey;
|
||||
private constructor();
|
||||
get mnemonic(): string;
|
||||
private get address();
|
||||
getAccounts(): Promise<readonly AccountData[]>;
|
||||
sign(address: string, message: Uint8Array): Promise<StdSignature>;
|
||||
}
|
1
packages/proto-signing/types/index.d.ts
vendored
1
packages/proto-signing/types/index.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
export { Coin } from "./msgs";
|
||||
export { cosmosField } from "./decorator";
|
||||
export { Registry } from "./registry";
|
||||
export { DirectSecp256k1Wallet } from "./directsecp256k1wallet";
|
||||
export { makeAuthInfo, makeSignBytes } from "./signing";
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Bech32, fromBase64 } from "@cosmjs/encoding";
|
||||
import { Coin, coins, Secp256k1Wallet } from "@cosmjs/launchpad";
|
||||
import { makeAuthInfo, makeSignBytes, Registry } from "@cosmjs/proto-signing";
|
||||
import { Coin, coins } from "@cosmjs/launchpad";
|
||||
import { DirectSecp256k1Wallet, makeAuthInfo, makeSignBytes, Registry } from "@cosmjs/proto-signing";
|
||||
import { assert, sleep } from "@cosmjs/utils";
|
||||
|
||||
import { cosmos } from "./codec";
|
||||
@ -27,7 +27,7 @@ interface TestTxSend {
|
||||
async function sendTokens(
|
||||
client: StargateClient,
|
||||
registry: Registry,
|
||||
wallet: Secp256k1Wallet,
|
||||
wallet: DirectSecp256k1Wallet,
|
||||
recipient: string,
|
||||
amount: readonly Coin[],
|
||||
memo: string,
|
||||
@ -83,7 +83,7 @@ describe("StargateClient.searchTx", () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
if (simappEnabled()) {
|
||||
const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const client = await StargateClient.connect(simapp.tendermintUrl);
|
||||
const unsuccessfulRecipient = makeRandomAddress();
|
||||
const successfulRecipient = makeRandomAddress();
|
||||
|
@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Bech32, fromBase64 } from "@cosmjs/encoding";
|
||||
import { Secp256k1Wallet } from "@cosmjs/launchpad";
|
||||
import { makeAuthInfo, makeSignBytes, Registry } from "@cosmjs/proto-signing";
|
||||
import { DirectSecp256k1Wallet, makeAuthInfo, makeSignBytes, Registry } from "@cosmjs/proto-signing";
|
||||
import { assert, sleep } from "@cosmjs/utils";
|
||||
import { ReadonlyDate } from "readonly-date";
|
||||
|
||||
@ -252,7 +251,7 @@ describe("StargateClient", () => {
|
||||
it("broadcasts a transaction", async () => {
|
||||
pendingWithoutSimapp();
|
||||
const client = await StargateClient.connect(simapp.tendermintUrl);
|
||||
const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const wallet = await DirectSecp256k1Wallet.fromMnemonic(faucet.mnemonic);
|
||||
const [{ address, pubkey: pubkeyBytes }] = await wallet.getAccounts();
|
||||
const publicKey = PublicKey.create({ secp256k1: pubkeyBytes });
|
||||
const registry = new Registry();
|
||||
|
Loading…
x
Reference in New Issue
Block a user