diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2c464f8f..f021d8a214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 0.22.1 (unreleased) + +- @cosmjs/cli: Import `encodeAminoPubkey`, `encodeBech32Pubkey`, + `decodeAminoPubkey` and `decodeBech32Pubkey` by default. +- @cosmjs/launchpad: Add ed25519 support to `encodeBech32Pubkey`. +- @cosmjs/launchpad: Add `encodeAminoPubkey` and `decodeAminoPubkey`. +- @cosmjs/utils: Add `arrayContentEquals`. + ## 0.22.0 (2020-08-03) - @cosmjs/cli: Now supports HTTPs URLs for `--init` code sources. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b256d2f480..7e5bcad3c1 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -89,6 +89,10 @@ export async function main(originalArgs: readonly string[]): Promise { [ "coin", "coins", + "decodeAminoPubkey", + "decodeBech32Pubkey", + "encodeAminoPubkey", + "encodeBech32Pubkey", "encodeSecp256k1Pubkey", "encodeSecp256k1Signature", "logs", @@ -112,7 +116,7 @@ export async function main(originalArgs: readonly string[]): Promise { ], ], ["@cosmjs/math", ["Decimal", "Int53", "Uint32", "Uint53", "Uint64"]], - ["@cosmjs/utils", ["assert", "sleep"]], + ["@cosmjs/utils", ["assert", "arrayContentEquals", "sleep"]], ]); console.info(colors.green("Initializing session for you. Have fun!")); @@ -156,6 +160,12 @@ export async function main(originalArgs: readonly string[]): Promise { const data = toAscii("foo bar"); const signature = await wallet.sign(address, data); + const bechPubkey = "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq"; + assert(encodeBech32Pubkey(decodeBech32Pubkey(bechPubkey), "coralvalconspub") == bechPubkey); + + const aminoPubkey = fromHex("eb5ae98721034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c70290"); + assert(arrayContentEquals(encodeAminoPubkey(decodeAminoPubkey(aminoPubkey)), aminoPubkey)); + console.info("Done testing, will exit now."); process.exit(0); `; diff --git a/packages/cosmwasm/package.json b/packages/cosmwasm/package.json index fadc0b497b..e0923c6b7d 100644 --- a/packages/cosmwasm/package.json +++ b/packages/cosmwasm/package.json @@ -44,7 +44,6 @@ "@cosmjs/math": "^0.22.0", "@cosmjs/utils": "^0.22.0", "axios": "^0.19.0", - "fast-deep-equal": "^3.1.1", "pako": "^1.0.11" }, "devDependencies": { diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index 7fe56adb7d..9b2ca12ae2 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -45,8 +45,7 @@ "@cosmjs/encoding": "^0.22.0", "@cosmjs/math": "^0.22.0", "@cosmjs/utils": "^0.22.0", - "axios": "^0.19.0", - "fast-deep-equal": "^3.1.1" + "axios": "^0.19.0" }, "devDependencies": { "readonly-date": "^1.0.0" diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 0149c07fd4..6d6e238886 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -84,7 +84,13 @@ export { uint64ToString, } from "./lcdapi"; export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs"; -export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; +export { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; diff --git a/packages/launchpad/src/pubkey.spec.ts b/packages/launchpad/src/pubkey.spec.ts index 03253695e4..9576239b88 100644 --- a/packages/launchpad/src/pubkey.spec.ts +++ b/packages/launchpad/src/pubkey.spec.ts @@ -1,6 +1,12 @@ -import { fromBase64 } from "@cosmjs/encoding"; +import { Bech32, fromBase64 } from "@cosmjs/encoding"; -import { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; +import { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./pubkey"; import { PubKey } from "./types"; describe("pubkey", () => { @@ -21,6 +27,30 @@ describe("pubkey", () => { }); }); + describe("decodeAminoPubkey", () => { + it("works for secp256k1", () => { + const amino = Bech32.decode( + "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", + ).data; + expect(decodeAminoPubkey(amino)).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }); + }); + + it("works for ed25519", () => { + // Encoded from `corald tendermint show-validator` + // Decoded from http://localhost:26657/validators + const amino = Bech32.decode( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ).data; + expect(decodeAminoPubkey(amino)).toEqual({ + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }); + }); + }); + describe("decodeBech32Pubkey", () => { it("works", () => { expect( @@ -39,6 +69,44 @@ describe("pubkey", () => { value: "A6lihrEs3PEFCu8m01ebcas3KjEVAjDIEmU7P9ED3PFx", }); }); + + it("works for ed25519", () => { + // Encoded from `corald tendermint show-validator` + // Decoded from http://localhost:26657/validators + const decoded = decodeBech32Pubkey( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ); + expect(decoded).toEqual({ + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }); + }); + }); + + describe("encodeAminoPubkey", () => { + it("works for secp256k1", () => { + const pubkey: PubKey = { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }; + const expected = Bech32.decode( + "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", + ).data; + expect(encodeAminoPubkey(pubkey)).toEqual(expected); + }); + + it("works for ed25519", () => { + // Decoded from http://localhost:26657/validators + // Encoded from `corald tendermint show-validator` + const pubkey: PubKey = { + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }; + const expected = Bech32.decode( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ).data; + expect(encodeAminoPubkey(pubkey)).toEqual(expected); + }); }); describe("encodeBech32Pubkey", () => { @@ -51,5 +119,17 @@ describe("pubkey", () => { "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5", ); }); + + it("works for ed25519", () => { + // Decoded from http://localhost:26657/validators + // Encoded from `corald tendermint show-validator` + const pubkey: PubKey = { + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }; + expect(encodeBech32Pubkey(pubkey, "coralvalconspub")).toEqual( + "coralvalconspub1zcjduepqvxg72ccnl9r65fv0wn3amlk4sfzqfe2k36l073kjx2qyaf6sk23qw7j8wq", + ); + }); }); }); diff --git a/packages/launchpad/src/pubkey.ts b/packages/launchpad/src/pubkey.ts index 2095c05a33..bebc3c62d6 100644 --- a/packages/launchpad/src/pubkey.ts +++ b/packages/launchpad/src/pubkey.ts @@ -1,5 +1,5 @@ import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; -import equal from "fast-deep-equal"; +import { arrayContentEquals } from "@cosmjs/utils"; import { PubKey, pubkeyType } from "./types"; @@ -21,12 +21,13 @@ const pubkeyAminoPrefixEd25519 = fromHex("1624de6420"); const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005"); const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; -export function decodeBech32Pubkey(bechEncoded: string): PubKey { - const { data } = Bech32.decode(bechEncoded); - +/** + * Decodes a pubkey in the Amino binary format to a type/value object. + */ +export function decodeAminoPubkey(data: Uint8Array): PubKey { const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); const rest = data.slice(pubkeyAminoPrefixLength); - if (equal(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { + if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { if (rest.length !== 33) { throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); } @@ -34,7 +35,7 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey { type: pubkeyType.secp256k1, value: toBase64(rest), }; - } else if (equal(aminoPrefix, pubkeyAminoPrefixEd25519)) { + } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixEd25519)) { if (rest.length !== 32) { throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey)."); } @@ -42,7 +43,7 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey { type: pubkeyType.ed25519, value: toBase64(rest), }; - } else if (equal(aminoPrefix, pubkeyAminoPrefixSr25519)) { + } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSr25519)) { if (rest.length !== 32) { throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey)."); } @@ -55,17 +56,42 @@ export function decodeBech32Pubkey(bechEncoded: string): PubKey { } } -export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string { +/** + * Decodes a bech32 pubkey to Amino binary, which is then decoded to a type/value object. + * The bech32 prefix is ignored and discareded. + * + * @param bechEncoded the bech32 encoded pubkey + */ +export function decodeBech32Pubkey(bechEncoded: string): PubKey { + const { data } = Bech32.decode(bechEncoded); + return decodeAminoPubkey(data); +} + +/** + * Encodes a public key to binary Amino. + */ +export function encodeAminoPubkey(pubkey: PubKey): Uint8Array { let aminoPrefix: Uint8Array; switch (pubkey.type) { // Note: please don't add cases here without writing additional unit tests case pubkeyType.secp256k1: aminoPrefix = pubkeyAminoPrefixSecp256k1; break; + case pubkeyType.ed25519: + aminoPrefix = pubkeyAminoPrefixEd25519; + break; default: throw new Error("Unsupported pubkey type"); } - - const data = new Uint8Array([...aminoPrefix, ...fromBase64(pubkey.value)]); - return Bech32.encode(prefix, data); + return new Uint8Array([...aminoPrefix, ...fromBase64(pubkey.value)]); +} + +/** + * Encodes a public key to binary Amino and then to bech32. + * + * @param pubkey the public key to encode + * @param prefix the bech32 prefix (human readable part) + */ +export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string { + return Bech32.encode(prefix, encodeAminoPubkey(pubkey)); } diff --git a/packages/launchpad/src/types.ts b/packages/launchpad/src/types.ts index e0de1e8465..dc79901451 100644 --- a/packages/launchpad/src/types.ts +++ b/packages/launchpad/src/types.ts @@ -37,7 +37,7 @@ export interface StdSignature { } export interface PubKey { - // type is one of the strings defined in pubkeyTypes + // type is one of the strings defined in pubkeyType // I don't use a string literal union here as that makes trouble with json test data: // https://github.com/CosmWasm/cosmjs/pull/44#pullrequestreview-353280504 readonly type: string; @@ -54,5 +54,3 @@ export const pubkeyType = { /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ sr25519: "tendermint/PubKeySr25519" as const, }; - -export const pubkeyTypes: readonly string[] = [pubkeyType.secp256k1, pubkeyType.ed25519, pubkeyType.sr25519]; diff --git a/packages/launchpad/types/index.d.ts b/packages/launchpad/types/index.d.ts index d8dfe32e4a..03c9a87d44 100644 --- a/packages/launchpad/types/index.d.ts +++ b/packages/launchpad/types/index.d.ts @@ -82,7 +82,13 @@ export { uint64ToString, } from "./lcdapi"; export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs"; -export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; +export { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; diff --git a/packages/launchpad/types/pubkey.d.ts b/packages/launchpad/types/pubkey.d.ts index b6ebdebc68..e39865126b 100644 --- a/packages/launchpad/types/pubkey.d.ts +++ b/packages/launchpad/types/pubkey.d.ts @@ -1,4 +1,24 @@ import { PubKey } from "./types"; export declare function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey; +/** + * Decodes a pubkey in the Amino binary format to a type/value object. + */ +export declare function decodeAminoPubkey(data: Uint8Array): PubKey; +/** + * Decodes a bech32 pubkey to Amino binary, which is then decoded to a type/value object. + * The bech32 prefix is ignored and discareded. + * + * @param bechEncoded the bech32 encoded pubkey + */ export declare function decodeBech32Pubkey(bechEncoded: string): PubKey; +/** + * Encodes a public key to binary Amino. + */ +export declare function encodeAminoPubkey(pubkey: PubKey): Uint8Array; +/** + * Encodes a public key to binary Amino and then to bech32. + * + * @param pubkey the public key to encode + * @param prefix the bech32 prefix (human readable part) + */ export declare function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string; diff --git a/packages/launchpad/types/types.d.ts b/packages/launchpad/types/types.d.ts index c12484fa5c..1a513b4168 100644 --- a/packages/launchpad/types/types.d.ts +++ b/packages/launchpad/types/types.d.ts @@ -36,4 +36,3 @@ export declare const pubkeyType: { /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ sr25519: "tendermint/PubKeySr25519"; }; -export declare const pubkeyTypes: readonly string[]; diff --git a/packages/utils/src/array.spec.ts b/packages/utils/src/array.spec.ts new file mode 100644 index 0000000000..d42241bcc6 --- /dev/null +++ b/packages/utils/src/array.spec.ts @@ -0,0 +1,33 @@ +import { arrayContentEquals } from "./arrays"; + +describe("array", () => { + describe("arrayContentEquals", () => { + it("can compare number arrays", () => { + expect(arrayContentEquals([1, 2, 3], [1, 2, 3])).toEqual(true); + expect(arrayContentEquals([1, 2, 3], [1, 2, 3, 4])).toEqual(false); + expect(arrayContentEquals([1, 2, 3], [3, 2, 1])).toEqual(false); + }); + + it("can compare string arrays", () => { + expect(arrayContentEquals(["a", "b"], ["a", "b"])).toEqual(true); + expect(arrayContentEquals(["a", "b"], ["a", "b", "c"])).toEqual(false); + expect(arrayContentEquals(["a", "b"], ["b", "a"])).toEqual(false); + }); + + it("can compare bool arrays", () => { + expect(arrayContentEquals([true, false], [true, false])).toEqual(true); + expect(arrayContentEquals([true, false], [true, false, true])).toEqual(false); + expect(arrayContentEquals([true, false], [false, true])).toEqual(false); + }); + + it("can compare different array types", () => { + expect(arrayContentEquals([1, 2, 3], new Uint8Array([1, 2, 3]))).toEqual(true); + expect(arrayContentEquals([1, 2, 3], new Uint8Array([3, 2, 1]))).toEqual(false); + }); + + it("works for empty arrays", () => { + expect(arrayContentEquals([], [])).toEqual(true); + expect(arrayContentEquals([], new Uint8Array([]))).toEqual(true); + }); + }); +}); diff --git a/packages/utils/src/arrays.ts b/packages/utils/src/arrays.ts new file mode 100644 index 0000000000..87f5a44244 --- /dev/null +++ b/packages/utils/src/arrays.ts @@ -0,0 +1,18 @@ +/** + * Compares the content of two arrays-like objects for equality. + * + * Equality is defined as having equal length and element values, where element equality means `===` returning `true`. + * + * This allows you to compare the content of a Buffer, Uint8Array or number[], ignoring the specific type. + * As a consequence, this returns different results than Jasmine's `toEqual`, which ensures elements have the same type. + */ +export function arrayContentEquals( + a: ArrayLike, + b: ArrayLike, +): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ea0963014e..38824e743f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +export { arrayContentEquals } from "./arrays"; export { assert } from "./assert"; export { sleep } from "./sleep"; export { isNonNullObject, isUint8Array } from "./typechecks"; diff --git a/packages/utils/types/arrays.d.ts b/packages/utils/types/arrays.d.ts new file mode 100644 index 0000000000..2d30e57991 --- /dev/null +++ b/packages/utils/types/arrays.d.ts @@ -0,0 +1,12 @@ +/** + * Compares the content of two arrays-like objects for equality. + * + * Equality is defined as having equal length and element values, where element equality means `===` returning `true`. + * + * This allows you to compare the content of a Buffer, Uint8Array or number[], ignoring the specific type. + * As a consequence, this returns different results than Jasmine's `toEqual`, which ensures elements have the same type. + */ +export declare function arrayContentEquals( + a: ArrayLike, + b: ArrayLike, +): boolean; diff --git a/packages/utils/types/index.d.ts b/packages/utils/types/index.d.ts index ea0963014e..38824e743f 100644 --- a/packages/utils/types/index.d.ts +++ b/packages/utils/types/index.d.ts @@ -1,3 +1,4 @@ +export { arrayContentEquals } from "./arrays"; export { assert } from "./assert"; export { sleep } from "./sleep"; export { isNonNullObject, isUint8Array } from "./typechecks";