Merge pull request #488 from cosmos/Secp256k1Wallet

Add Secp256k1Wallet
This commit is contained in:
Simon Warta 2020-10-27 15:17:55 +01:00 committed by GitHub
commit 7feea61e42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 149 additions and 2 deletions

View File

@ -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)

View File

@ -117,6 +117,7 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
"PubKey",
"pubkeyToAddress",
"Secp256k1HdWallet",
"Secp256k1Wallet",
"SigningCosmosClient",
"StdFee",
"StdSignDoc",
@ -162,6 +163,10 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
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();

View File

@ -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";

View File

@ -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: [],

View File

@ -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);
});
});
});

View File

@ -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<Secp256k1Wallet> {
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<readonly AccountData[]> {
return [
{
algo: "secp256k1",
address: this.address,
pubkey: this.pubkey,
},
];
}
public async sign(signerAddress: string, signDoc: StdSignDoc): Promise<SignResponse> {
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),
};
}
}

View File

@ -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";

View File

@ -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<Secp256k1Wallet>;
private readonly pubkey;
private readonly privkey;
private readonly prefix;
private constructor();
private get address();
getAccounts(): Promise<readonly AccountData[]>;
sign(signerAddress: string, signDoc: StdSignDoc): Promise<SignResponse>;
}

View File

@ -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);