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/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 new file mode 100644 index 00000000..93d63682 --- /dev/null +++ b/packages/launchpad/src/secp256k1wallet.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 { Secp256k1Wallet } from "./secp256k1wallet"; + +describe("Secp256k1Wallet", () => { + const defaultPrivkey = fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e"); + const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; + const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); + + describe("fromKey", () => { + it("works", async () => { + const signer = await Secp256k1Wallet.fromKey(defaultPrivkey); + expect(signer).toBeTruthy(); + }); + }); + + describe("getAccounts", () => { + it("resolves to a list of accounts", async () => { + const signer = await Secp256k1Wallet.fromKey(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", async () => { + const signer = await Secp256k1Wallet.fromKey(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/secp256k1wallet.ts b/packages/launchpad/src/secp256k1wallet.ts new file mode 100644 index 00000000..7580d484 --- /dev/null +++ b/packages/launchpad/src/secp256k1wallet.ts @@ -0,0 +1,61 @@ +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"; + +/** + * 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 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 fromKey(privkey: Uint8Array, prefix = "cosmos"): Promise { + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new Secp256k1Wallet(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/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"; diff --git a/packages/launchpad/types/secp256k1wallet.d.ts b/packages/launchpad/types/secp256k1wallet.d.ts new file mode 100644 index 00000000..ef0f87e7 --- /dev/null +++ b/packages/launchpad/types/secp256k1wallet.d.ts @@ -0,0 +1,23 @@ +import { StdSignDoc } from "./encoding"; +import { AccountData, OfflineSigner, SignResponse } from "./signer"; +/** + * 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 Secp256k1Wallet from the given private key + * + * @param privkey The private key. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + static fromKey(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; +} 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);