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; +}