From 329cf17a0d8c0ae325e4081cc0b6c8ab8553e87b Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 11 Nov 2020 11:59:37 -0800 Subject: [PATCH] Add DirectSecp256k1Wallet --- .../src/directsecp256k1wallet.spec.ts | 64 +++++++++++++++++++ .../src/directsecp256k1wallet.ts | 63 ++++++++++++++++++ .../types/directsecp256k1wallet.d.ts | 24 +++++++ 3 files changed, 151 insertions(+) create mode 100644 packages/proto-signing/src/directsecp256k1wallet.spec.ts create mode 100644 packages/proto-signing/src/directsecp256k1wallet.ts create mode 100644 packages/proto-signing/types/directsecp256k1wallet.d.ts diff --git a/packages/proto-signing/src/directsecp256k1wallet.spec.ts b/packages/proto-signing/src/directsecp256k1wallet.spec.ts new file mode 100644 index 00000000..1b17d381 --- /dev/null +++ b/packages/proto-signing/src/directsecp256k1wallet.spec.ts @@ -0,0 +1,64 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; +import { fromBase64, fromHex } from "@cosmjs/encoding"; +import { coins } from "@cosmjs/launchpad"; + +import { DirectSecp256k1Wallet } from "./directsecp256k1wallet"; +import { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing"; +import { testVectors } from "./testutils.spec"; + +describe("DirectSecp256k1Wallet", () => { + const defaultPrivkey = fromHex("b8c462d2bb0c1a92edf44f735021f16c270f28ee2c3d1cb49943a5e70a3c763e"); + const defaultAddress = "cosmos1kxt5x5q2l57ma2d434pqpafxdm0mgeg9c8cvtx"; + const defaultPubkey = fromHex("03f146c27639179e5b67b8646108f48e1a78b146c74939e34afaa5414ad5c93f8a"); + + describe("fromKey", () => { + it("works", async () => { + const signer = await DirectSecp256k1Wallet.fromKey(defaultPrivkey); + expect(signer).toBeTruthy(); + }); + }); + + describe("getAccounts", () => { + it("resolves to a list of accounts", async () => { + const signer = await DirectSecp256k1Wallet.fromKey(defaultPrivkey); + const accounts = await signer.getAccounts(); + expect(accounts.length).toEqual(1); + expect(accounts[0]).toEqual({ + address: defaultAddress, + algo: "secp256k1", + pubkey: defaultPubkey, + }); + }); + }); + + describe("signDirect", () => { + it("resolves to valid signature", async () => { + const { sequence, bodyBytes } = testVectors[1]; + const wallet = await DirectSecp256k1Wallet.fromKey(defaultPrivkey); + const accounts = await wallet.getAccounts(); + const pubkey = { + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: accounts[0].pubkey, + }; + const fee = coins(2000, "ucosm"); + const gasLimit = 200000; + const chainId = "simd-testing"; + const accountNumber = 1; + const signDoc = makeSignDoc( + fromHex(bodyBytes), + makeAuthInfoBytes([pubkey], fee, gasLimit, sequence), + chainId, + accountNumber, + ); + const signDocBytes = makeSignBytes(signDoc); + const { signature } = await wallet.signDirect(accounts[0].address, signDoc); + const valid = await Secp256k1.verifySignature( + Secp256k1Signature.fromFixedLength(fromBase64(signature.signature)), + sha256(signDocBytes), + pubkey.value, + ); + expect(valid).toEqual(true); + }); + }); +}); diff --git a/packages/proto-signing/src/directsecp256k1wallet.ts b/packages/proto-signing/src/directsecp256k1wallet.ts new file mode 100644 index 00000000..8f312152 --- /dev/null +++ b/packages/proto-signing/src/directsecp256k1wallet.ts @@ -0,0 +1,63 @@ +import { Secp256k1, sha256 } from "@cosmjs/crypto"; +import { AccountData, encodeSecp256k1Signature, rawSecp256k1PubkeyToAddress } from "@cosmjs/launchpad"; + +import { cosmos } from "./codec"; +import { DirectSignResponse, OfflineDirectSigner } from "./signer"; +import { makeSignBytes } from "./signing"; + +/** + * A wallet that holds a single secp256k1 keypair. + * + * If you want to work with BIP39 mnemonics and multiple accounts, use DirectSecp256k1HdWallet. + */ +export class DirectSecp256k1Wallet implements OfflineDirectSigner { + /** + * Creates a DirectSecp256k1Wallet 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 DirectSecp256k1Wallet(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 signDirect(address: string, signDoc: cosmos.tx.v1beta1.ISignDoc): Promise { + const signBytes = makeSignBytes(signDoc); + if (address !== this.address) { + throw new Error(`Address ${address} not found in wallet`); + } + const hashedMessage = sha256(signBytes); + const signature = await Secp256k1.createSignature(hashedMessage, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + const stdSignature = encodeSecp256k1Signature(this.pubkey, signatureBytes); + return { + signed: signDoc, + signature: stdSignature, + }; + } +} diff --git a/packages/proto-signing/types/directsecp256k1wallet.d.ts b/packages/proto-signing/types/directsecp256k1wallet.d.ts new file mode 100644 index 00000000..e5fc3808 --- /dev/null +++ b/packages/proto-signing/types/directsecp256k1wallet.d.ts @@ -0,0 +1,24 @@ +import { AccountData } from "@cosmjs/launchpad"; +import { cosmos } from "./codec"; +import { DirectSignResponse, OfflineDirectSigner } from "./signer"; +/** + * A wallet that holds a single secp256k1 keypair. + * + * If you want to work with BIP39 mnemonics and multiple accounts, use DirectSecp256k1HdWallet. + */ +export declare class DirectSecp256k1Wallet implements OfflineDirectSigner { + /** + * Creates a DirectSecp256k1Wallet 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; + signDirect(address: string, signDoc: cosmos.tx.v1beta1.ISignDoc): Promise; +}