From 04a74aac839d2d748df416f244c8c861d68afb94 Mon Sep 17 00:00:00 2001 From: Shane Vitarana Date: Wed, 7 Oct 2020 12:03:07 -0400 Subject: [PATCH 1/5] Add Secpk256k1 offline key signer Renamed Secp256k1Wallet -> Secp256k1HdWallet Renamed Secp256k1Key -> Secp256k1Wallet Renamed fromPrivateKey function Fixed linter issues Rename Secp256k1Wallet.fromMnemonic everywhere Revert "Rename Secp256k1Wallet.fromMnemonic everywhere" This reverts commit 18c52968bc1a234755c61d1d5c67850222239bd7. Revert "Fixed linter issues" This reverts commit 8e6fce93d809f160e8dafa9478f56f9effcf561a. Revert "Renamed fromPrivateKey function" This reverts commit d628f186bb126d4fc2b205dbc0b95b3198af02df. Revert "Renamed Secp256k1Key -> Secp256k1Wallet" This reverts commit 35a082701de9bd2c8cf89b25a168f12212d4ba8a. Revert "Renamed Secp256k1Wallet -> Secp256k1HdWallet" This reverts commit bd69f293e1ae7d96ad0afd295af360de70863e13. Ran linter Renamed fromPrivateKey Updated Typescript definitions --- packages/launchpad/src/secp256k1key.spec.ts | 54 ++++++++++++++++++++ packages/launchpad/src/secp256k1key.ts | 56 +++++++++++++++++++++ packages/launchpad/types/secp256k1key.d.ts | 18 +++++++ 3 files changed, 128 insertions(+) create mode 100644 packages/launchpad/src/secp256k1key.spec.ts create mode 100644 packages/launchpad/src/secp256k1key.ts create mode 100644 packages/launchpad/types/secp256k1key.d.ts diff --git a/packages/launchpad/src/secp256k1key.spec.ts b/packages/launchpad/src/secp256k1key.spec.ts new file mode 100644 index 00000000..643a4b21 --- /dev/null +++ b/packages/launchpad/src/secp256k1key.spec.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; +import { fromBase64, fromHex } from "@cosmjs/encoding"; + +import { serializeSignDoc, StdSignDoc } from "./encoding"; +import { Secp256k1Key } from "./secp256k1key"; + +describe("Secp256k1Key", () => { + const defaultPrivkey = fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e"); + const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; + const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); + + describe("fromPrivkey", () => { + it("works", async () => { + const signer = await Secp256k1Key.fromPrivkey(defaultPrivkey); + expect(signer).toBeTruthy(); + }); + }); + + describe("getAccounts", () => { + it("resolves to a list of accounts", async () => { + const signer = await Secp256k1Key.fromPrivkey(defaultPrivkey); + const accounts = await signer.getAccounts(); + expect(accounts.length).toEqual(1); + expect(accounts[0]).toEqual({ + address: defaultAddress, + algo: "secp256k1", + pubkey: defaultPubkey, + }); + }); + }); + + describe("sign", () => { + it("resolves to valid signature if enabled", async () => { + const signer = await Secp256k1Key.fromPrivkey(defaultPrivkey); + const signDoc: StdSignDoc = { + msgs: [], + fee: { amount: [], gas: "23" }, + chain_id: "foochain", + memo: "hello, world", + account_number: "7", + sequence: "54", + }; + const { signed, signature } = await signer.sign(defaultAddress, signDoc); + expect(signed).toEqual(signDoc); + const valid = await Secp256k1.verifySignature( + Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), + new Sha256(serializeSignDoc(signed)).digest(), + defaultPubkey, + ); + expect(valid).toEqual(true); + }); + }); +}); diff --git a/packages/launchpad/src/secp256k1key.ts b/packages/launchpad/src/secp256k1key.ts new file mode 100644 index 00000000..33bde15f --- /dev/null +++ b/packages/launchpad/src/secp256k1key.ts @@ -0,0 +1,56 @@ +import { Secp256k1, Sha256 } from "@cosmjs/crypto"; + +import { rawSecp256k1PubkeyToAddress } from "./address"; +import { serializeSignDoc, StdSignDoc } from "./encoding"; +import { encodeSecp256k1Signature } from "./signature"; +import { AccountData, OfflineSigner, SignResponse } from "./signer"; + +export class Secp256k1Key implements OfflineSigner { + /** + * Creates a Secp256k1 key signer from the given private key + * + * @param privkey The private key. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async fromPrivkey(privkey: Uint8Array, prefix = "cosmos"): Promise { + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new Secp256k1Key(privkey, Secp256k1.compressPubkey(uncompressed), prefix); + } + + private readonly pubkey: Uint8Array; + private readonly privkey: Uint8Array; + private readonly prefix: string; + + private constructor(privkey: Uint8Array, pubkey: Uint8Array, prefix: string) { + this.privkey = privkey; + this.pubkey = pubkey; + this.prefix = prefix; + } + + private get address(): string { + return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix); + } + + public async getAccounts(): Promise { + return [ + { + algo: "secp256k1", + address: this.address, + pubkey: this.pubkey, + }, + ]; + } + + public async sign(signerAddress: string, signDoc: StdSignDoc): Promise { + if (signerAddress !== this.address) { + throw new Error(`Address ${signerAddress} not found in wallet`); + } + const message = new Sha256(serializeSignDoc(signDoc)).digest(); + const signature = await Secp256k1.createSignature(message, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return { + signed: signDoc, + signature: encodeSecp256k1Signature(this.pubkey, signatureBytes), + }; + } +} diff --git a/packages/launchpad/types/secp256k1key.d.ts b/packages/launchpad/types/secp256k1key.d.ts new file mode 100644 index 00000000..1f5d6edc --- /dev/null +++ b/packages/launchpad/types/secp256k1key.d.ts @@ -0,0 +1,18 @@ +import { StdSignDoc } from "./encoding"; +import { AccountData, OfflineSigner, SignResponse } from "./signer"; +export declare class Secp256k1Key implements OfflineSigner { + /** + * Creates a Secp256k1 key signer from the given private key + * + * @param privkey The private key. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + static fromPrivkey(privkey: Uint8Array, prefix?: string): Promise; + private readonly pubkey; + private readonly privkey; + private readonly prefix; + private constructor(); + private get address(); + getAccounts(): Promise; + sign(signerAddress: string, signDoc: StdSignDoc): Promise; +} From 2921c15ea01d0cee2fb21ba60be1a5d749313ff9 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Oct 2020 13:41:54 +0100 Subject: [PATCH 2/5] Rename to Secp256k1Wallet --- .../{secp256k1key.spec.ts => secp256k1wallet.spec.ts} | 10 +++++----- .../src/{secp256k1key.ts => secp256k1wallet.ts} | 11 ++++++++--- .../types/{secp256k1key.d.ts => secp256k1wallet.d.ts} | 9 +++++++-- 3 files changed, 20 insertions(+), 10 deletions(-) rename packages/launchpad/src/{secp256k1key.spec.ts => secp256k1wallet.spec.ts} (84%) rename packages/launchpad/src/{secp256k1key.ts => secp256k1wallet.ts} (84%) rename packages/launchpad/types/{secp256k1key.d.ts => secp256k1wallet.d.ts} (72%) diff --git a/packages/launchpad/src/secp256k1key.spec.ts b/packages/launchpad/src/secp256k1wallet.spec.ts similarity index 84% rename from packages/launchpad/src/secp256k1key.spec.ts rename to packages/launchpad/src/secp256k1wallet.spec.ts index 643a4b21..6ba21057 100644 --- a/packages/launchpad/src/secp256k1key.spec.ts +++ b/packages/launchpad/src/secp256k1wallet.spec.ts @@ -3,23 +3,23 @@ import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex } from "@cosmjs/encoding"; import { serializeSignDoc, StdSignDoc } from "./encoding"; -import { Secp256k1Key } from "./secp256k1key"; +import { Secp256k1Wallet } from "./secp256k1wallet"; -describe("Secp256k1Key", () => { +describe("Secp256k1Wallet", () => { const defaultPrivkey = fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e"); const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); describe("fromPrivkey", () => { it("works", async () => { - const signer = await Secp256k1Key.fromPrivkey(defaultPrivkey); + const signer = await Secp256k1Wallet.fromPrivkey(defaultPrivkey); expect(signer).toBeTruthy(); }); }); describe("getAccounts", () => { it("resolves to a list of accounts", async () => { - const signer = await Secp256k1Key.fromPrivkey(defaultPrivkey); + const signer = await Secp256k1Wallet.fromPrivkey(defaultPrivkey); const accounts = await signer.getAccounts(); expect(accounts.length).toEqual(1); expect(accounts[0]).toEqual({ @@ -32,7 +32,7 @@ describe("Secp256k1Key", () => { describe("sign", () => { it("resolves to valid signature if enabled", async () => { - const signer = await Secp256k1Key.fromPrivkey(defaultPrivkey); + const signer = await Secp256k1Wallet.fromPrivkey(defaultPrivkey); const signDoc: StdSignDoc = { msgs: [], fee: { amount: [], gas: "23" }, diff --git a/packages/launchpad/src/secp256k1key.ts b/packages/launchpad/src/secp256k1wallet.ts similarity index 84% rename from packages/launchpad/src/secp256k1key.ts rename to packages/launchpad/src/secp256k1wallet.ts index 33bde15f..174a6b79 100644 --- a/packages/launchpad/src/secp256k1key.ts +++ b/packages/launchpad/src/secp256k1wallet.ts @@ -5,16 +5,21 @@ import { serializeSignDoc, StdSignDoc } from "./encoding"; import { encodeSecp256k1Signature } from "./signature"; import { AccountData, OfflineSigner, SignResponse } from "./signer"; -export class Secp256k1Key implements OfflineSigner { +/** + * A wallet that holds a single secp256k1 keypair. + * + * If you want to work with BIP39 mnemonics and multiple accounts, use Secp256k1HdWallet. + */ +export class Secp256k1Wallet implements OfflineSigner { /** * Creates a Secp256k1 key signer from the given private key * * @param privkey The private key. * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". */ - public static async fromPrivkey(privkey: Uint8Array, prefix = "cosmos"): Promise { + public static async fromPrivkey(privkey: Uint8Array, prefix = "cosmos"): Promise { const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; - return new Secp256k1Key(privkey, Secp256k1.compressPubkey(uncompressed), prefix); + return new Secp256k1Wallet(privkey, Secp256k1.compressPubkey(uncompressed), prefix); } private readonly pubkey: Uint8Array; diff --git a/packages/launchpad/types/secp256k1key.d.ts b/packages/launchpad/types/secp256k1wallet.d.ts similarity index 72% rename from packages/launchpad/types/secp256k1key.d.ts rename to packages/launchpad/types/secp256k1wallet.d.ts index 1f5d6edc..401201d8 100644 --- a/packages/launchpad/types/secp256k1key.d.ts +++ b/packages/launchpad/types/secp256k1wallet.d.ts @@ -1,13 +1,18 @@ import { StdSignDoc } from "./encoding"; import { AccountData, OfflineSigner, SignResponse } from "./signer"; -export declare class Secp256k1Key implements OfflineSigner { +/** + * A wallet that holds a single secp256k1 keypair. + * + * If you want to work with BIP39 mnemonics and multiple accounts, use Secp256k1HdWallet. + */ +export declare class Secp256k1Wallet implements OfflineSigner { /** * Creates a Secp256k1 key signer from the given private key * * @param privkey The private key. * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". */ - static fromPrivkey(privkey: Uint8Array, prefix?: string): Promise; + static fromPrivkey(privkey: Uint8Array, prefix?: string): Promise; private readonly pubkey; private readonly privkey; private readonly prefix; From 0091d9a6848e212f42aef8b4a667f04b59e97443 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Oct 2020 13:43:03 +0100 Subject: [PATCH 3/5] Rename to Secp256k1Wallet.fromKey --- packages/launchpad/src/secp256k1wallet.spec.ts | 8 ++++---- packages/launchpad/src/secp256k1wallet.ts | 4 ++-- packages/launchpad/types/secp256k1wallet.d.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/launchpad/src/secp256k1wallet.spec.ts b/packages/launchpad/src/secp256k1wallet.spec.ts index 6ba21057..8f638ab2 100644 --- a/packages/launchpad/src/secp256k1wallet.spec.ts +++ b/packages/launchpad/src/secp256k1wallet.spec.ts @@ -10,16 +10,16 @@ describe("Secp256k1Wallet", () => { const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); - describe("fromPrivkey", () => { + describe("fromKey", () => { it("works", async () => { - const signer = await Secp256k1Wallet.fromPrivkey(defaultPrivkey); + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); expect(signer).toBeTruthy(); }); }); describe("getAccounts", () => { it("resolves to a list of accounts", async () => { - const signer = await Secp256k1Wallet.fromPrivkey(defaultPrivkey); + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); const accounts = await signer.getAccounts(); expect(accounts.length).toEqual(1); expect(accounts[0]).toEqual({ @@ -32,7 +32,7 @@ describe("Secp256k1Wallet", () => { describe("sign", () => { it("resolves to valid signature if enabled", async () => { - const signer = await Secp256k1Wallet.fromPrivkey(defaultPrivkey); + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); const signDoc: StdSignDoc = { msgs: [], fee: { amount: [], gas: "23" }, diff --git a/packages/launchpad/src/secp256k1wallet.ts b/packages/launchpad/src/secp256k1wallet.ts index 174a6b79..7580d484 100644 --- a/packages/launchpad/src/secp256k1wallet.ts +++ b/packages/launchpad/src/secp256k1wallet.ts @@ -12,12 +12,12 @@ import { AccountData, OfflineSigner, SignResponse } from "./signer"; */ export class Secp256k1Wallet implements OfflineSigner { /** - * Creates a Secp256k1 key signer from the given private key + * Creates a Secp256k1Wallet from the given private key * * @param privkey The private key. * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". */ - public static async fromPrivkey(privkey: Uint8Array, prefix = "cosmos"): Promise { + public static async fromKey(privkey: Uint8Array, prefix = "cosmos"): Promise { const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; return new Secp256k1Wallet(privkey, Secp256k1.compressPubkey(uncompressed), prefix); } diff --git a/packages/launchpad/types/secp256k1wallet.d.ts b/packages/launchpad/types/secp256k1wallet.d.ts index 401201d8..ef0f87e7 100644 --- a/packages/launchpad/types/secp256k1wallet.d.ts +++ b/packages/launchpad/types/secp256k1wallet.d.ts @@ -7,12 +7,12 @@ import { AccountData, OfflineSigner, SignResponse } from "./signer"; */ export declare class Secp256k1Wallet implements OfflineSigner { /** - * Creates a Secp256k1 key signer from the given private key + * Creates a Secp256k1Wallet from the given private key * * @param privkey The private key. * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". */ - static fromPrivkey(privkey: Uint8Array, prefix?: string): Promise; + static fromKey(privkey: Uint8Array, prefix?: string): Promise; private readonly pubkey; private readonly privkey; private readonly prefix; From 850dbd11db8873bcc38c69f5d377b5ab93478064 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Oct 2020 14:32:27 +0100 Subject: [PATCH 4/5] Expose Secp256k1Wallet --- CHANGELOG.md | 2 ++ packages/cli/src/cli.ts | 5 +++++ packages/launchpad/src/index.ts | 1 + packages/launchpad/types/index.d.ts | 1 + 4 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2364ff81..6be723bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - @cosmjs/cosmwasm: `logs` is no longer exported. Use `logs` from @cosmjs/launchpad instead. +- @cosmjs/launchpad: Add `Secp256k1Wallet` to manage a single raw secp256k1 + keypair. ## 0.23.1 (2020-10-27) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 2633a8e9..c0cb5a12 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -117,6 +117,7 @@ export async function main(originalArgs: readonly string[]): Promise { "PubKey", "pubkeyToAddress", "Secp256k1HdWallet", + "Secp256k1Wallet", "SigningCosmosClient", "StdFee", "StdSignDoc", @@ -162,6 +163,10 @@ export async function main(originalArgs: readonly string[]): Promise { assert(Decimal.fromAtomics("12870000", 6).toString() === "12.87"); + const oneKeyWallet = await Secp256k1Wallet.fromKey(fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e")); + const accounts = await oneKeyWallet.getAccounts(); + assert(accounts[0].address == "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"); + const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, makeCosmoshubPath(0)); const [{ address }] = await wallet.getAccounts(); diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 324b0fd2..fb3cd0a8 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -117,3 +117,4 @@ export { isStdTx, isWrappedStdTx, makeStdTx, CosmosSdkTx, StdTx, WrappedStdTx, W export { pubkeyType, PubKey, StdFee, StdSignature } from "./types"; export { makeCosmoshubPath, executeKdf, KdfConfiguration } from "./wallet"; export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; +export { Secp256k1Wallet } from "./secp256k1wallet"; diff --git a/packages/launchpad/types/index.d.ts b/packages/launchpad/types/index.d.ts index fe5b08ea..6580a9a2 100644 --- a/packages/launchpad/types/index.d.ts +++ b/packages/launchpad/types/index.d.ts @@ -115,3 +115,4 @@ export { isStdTx, isWrappedStdTx, makeStdTx, CosmosSdkTx, StdTx, WrappedStdTx, W export { pubkeyType, PubKey, StdFee, StdSignature } from "./types"; export { makeCosmoshubPath, executeKdf, KdfConfiguration } from "./wallet"; export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; +export { Secp256k1Wallet } from "./secp256k1wallet"; From a24c923777b7591680d15f417c5f2130a68da781 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Oct 2020 14:40:00 +0100 Subject: [PATCH 5/5] Remove obsolete "if enabled" in test descriptions --- packages/launchpad/src/secp256k1hdwallet.spec.ts | 2 +- packages/launchpad/src/secp256k1wallet.spec.ts | 2 +- packages/proto-signing/src/directsecp256k1wallet.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/launchpad/src/secp256k1hdwallet.spec.ts b/packages/launchpad/src/secp256k1hdwallet.spec.ts index f053accc..0354365e 100644 --- a/packages/launchpad/src/secp256k1hdwallet.spec.ts +++ b/packages/launchpad/src/secp256k1hdwallet.spec.ts @@ -110,7 +110,7 @@ describe("Secp256k1HdWallet", () => { }); describe("sign", () => { - it("resolves to valid signature if enabled", async () => { + it("resolves to valid signature", async () => { const wallet = await Secp256k1HdWallet.fromMnemonic(defaultMnemonic); const signDoc: StdSignDoc = { msgs: [], diff --git a/packages/launchpad/src/secp256k1wallet.spec.ts b/packages/launchpad/src/secp256k1wallet.spec.ts index 8f638ab2..93d63682 100644 --- a/packages/launchpad/src/secp256k1wallet.spec.ts +++ b/packages/launchpad/src/secp256k1wallet.spec.ts @@ -31,7 +31,7 @@ describe("Secp256k1Wallet", () => { }); describe("sign", () => { - it("resolves to valid signature if enabled", async () => { + it("resolves to valid signature", async () => { const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); const signDoc: StdSignDoc = { msgs: [], diff --git a/packages/proto-signing/src/directsecp256k1wallet.spec.ts b/packages/proto-signing/src/directsecp256k1wallet.spec.ts index 56eae17b..80e4f266 100644 --- a/packages/proto-signing/src/directsecp256k1wallet.spec.ts +++ b/packages/proto-signing/src/directsecp256k1wallet.spec.ts @@ -55,7 +55,7 @@ describe("DirectSecp256k1Wallet", () => { }); describe("sign", () => { - it("resolves to valid signature if enabled", async () => { + it("resolves to valid signature", async () => { const wallet = await DirectSecp256k1Wallet.fromMnemonic(defaultMnemonic); const message = toAscii("foo bar"); const signature = await wallet.sign(defaultAddress, message);