diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a6e5fb..da09a6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ and this project adheres to connection tx, as well as Tendermint. - @cosmjs/stargate: Add support for IBC message types in `SigningStargateClient`. +- @cosmjs/amino: New package created that contains the shared amino signing + functionality for @cosmjs/launchpad and @cosmjs/stargate. +- @cosmjs/amino: Split public key interfaces into `Pubkey`, `SinglePubkey` and + `Secp256k1Pubkey` where `Pubkey` is a generalization of the old `PubKey` that + supported nested pubkeys for multisig. `SinglePubkey` is the old `PubKey` in + which the `value` is a base64 encoded string. And `Secp256k1Pubkey` is a + single secp256k1 pubkey. +- @cosmjs/utils: The new `arrayContentStartsWith` works similar to + `arrayContentEquals` but only checks the start of an array. ### Changed @@ -40,6 +49,9 @@ and this project adheres to `blockIdFlag` is `BlockIdFlag.Absent`. The decoding into `CommitSignature` is only updated for the class `Tendermint34Client`, not for `Client`. Please migrate to the former. +- @cosmjs/launchpad: `rawSecp256k1PubkeyToAddress` was removed. Instead use + `Bech32.encode(prefix, rawSecp256k1PubkeyToRawAddress(pubkeyRaw))` with + `rawSecp256k1PubkeyToRawAddress` from @cosmjs/amino. ### Deprecated diff --git a/packages/amino/.eslintignore b/packages/amino/.eslintignore new file mode 100644 index 00000000..f373a53f --- /dev/null +++ b/packages/amino/.eslintignore @@ -0,0 +1,8 @@ +node_modules/ + +build/ +custom_types/ +dist/ +docs/ +generated/ +types/ diff --git a/packages/amino/.gitignore b/packages/amino/.gitignore new file mode 100644 index 00000000..68bf3735 --- /dev/null +++ b/packages/amino/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +docs/ diff --git a/packages/amino/.nycrc.yml b/packages/amino/.nycrc.yml new file mode 120000 index 00000000..1f95ac55 --- /dev/null +++ b/packages/amino/.nycrc.yml @@ -0,0 +1 @@ +../../.nycrc.yml \ No newline at end of file diff --git a/packages/amino/README.md b/packages/amino/README.md new file mode 100644 index 00000000..31c09c37 --- /dev/null +++ b/packages/amino/README.md @@ -0,0 +1,12 @@ +# @cosmjs/amino + +[![npm version](https://img.shields.io/npm/v/@cosmjs/amino.svg)](https://www.npmjs.com/package/@cosmjs/amino) + +Helpers for Amino based signing which are shared between @cosmjs/launchpad and +@cosmjs/stargate. + +## License + +This package is part of the cosmjs repository, licensed under the Apache License +2.0 (see [NOTICE](https://github.com/cosmos/cosmjs/blob/main/NOTICE) and +[LICENSE](https://github.com/cosmos/cosmjs/blob/main/LICENSE)). diff --git a/packages/amino/jasmine-testrunner.js b/packages/amino/jasmine-testrunner.js new file mode 100755 index 00000000..6558a133 --- /dev/null +++ b/packages/amino/jasmine-testrunner.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/naming-convention */ + +if (process.env.SES_ENABLED) { + require("ses/lockdown"); + // eslint-disable-next-line no-undef + lockdown(); +} + +require("source-map-support").install(); +const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json"); + +// setup Jasmine +const Jasmine = require("jasmine"); +const jasmine = new Jasmine(); +jasmine.loadConfig({ + spec_dir: "build", + spec_files: ["**/*.spec.js"], + helpers: [], + random: false, + seed: null, + stopSpecOnExpectationFailure: false, +}); +jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000; + +// setup reporter +const { SpecReporter } = require("jasmine-spec-reporter"); +const reporter = new SpecReporter({ + ...defaultSpecReporterConfig, + spec: { + ...defaultSpecReporterConfig.spec, + displaySuccessful: !process.argv.includes("--quiet"), + }, +}); + +// initialize and execute +jasmine.env.clearReporters(); +jasmine.addReporter(reporter); +jasmine.execute(); diff --git a/packages/amino/karma.conf.js b/packages/amino/karma.conf.js new file mode 100644 index 00000000..006da5fe --- /dev/null +++ b/packages/amino/karma.conf.js @@ -0,0 +1,47 @@ +module.exports = function (config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: ".", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["jasmine"], + + // list of files / patterns to load in the browser + files: ["dist/web/tests.js"], + + client: { + jasmine: { + random: false, + timeoutInterval: 15000, + }, + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["progress", "kjhtml"], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ["Firefox"], + + browserNoActivityTimeout: 90000, + + // Keep brower open for debugging. This is overridden by yarn scripts + singleRun: false, + }); +}; diff --git a/packages/amino/nonces/README.txt b/packages/amino/nonces/README.txt new file mode 100644 index 00000000..092fe732 --- /dev/null +++ b/packages/amino/nonces/README.txt @@ -0,0 +1 @@ +Directory used to trigger lerna package updates for all packages diff --git a/packages/amino/package.json b/packages/amino/package.json new file mode 100644 index 00000000..0df89f2b --- /dev/null +++ b/packages/amino/package.json @@ -0,0 +1,49 @@ +{ + "name": "@cosmjs/amino", + "version": "0.25.0-alpha.0", + "description": "Helpers for Amino based signing which are shared between @cosmjs/launchpad and @cosmjs/stargate.", + "contributors": [ + "Simon Warta " + ], + "license": "Apache-2.0", + "main": "build/index.js", + "types": "build/index.d.ts", + "files": [ + "build/", + "*.md", + "!*.spec.*", + "!**/testdata/" + ], + "repository": { + "type": "git", + "url": "https://github.com/cosmos/cosmjs/tree/main/packages/amino" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "docs": "typedoc --options typedoc.js", + "lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"", + "lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix", + "format": "prettier --write --loglevel warn \"./src/**/*.ts\"", + "format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"", + "test-node": "node jasmine-testrunner.js", + "test-edge": "yarn pack-web && karma start --single-run --browsers Edge", + "test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox", + "test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadless", + "test-safari": "yarn pack-web && karma start --single-run --browsers Safari", + "test": "yarn build-or-skip && yarn test-node", + "coverage": "nyc --reporter=text --reporter=lcov yarn test --quiet", + "prebuild": "shx rm -rf ./build", + "build": "tsc", + "build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build", + "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" + }, + "dependencies": { + "@cosmjs/crypto": "^0.25.0-alpha.0", + "@cosmjs/encoding": "^0.25.0-alpha.0", + "@cosmjs/utils": "^0.25.0-alpha.0" + }, + "devDependencies": { + } +} diff --git a/packages/amino/src/addresses.spec.ts b/packages/amino/src/addresses.spec.ts new file mode 100644 index 00000000..de90b7b9 --- /dev/null +++ b/packages/amino/src/addresses.spec.ts @@ -0,0 +1,91 @@ +import { Bech32, fromHex, toBase64 } from "@cosmjs/encoding"; + +import { pubkeyToAddress, pubkeyToRawAddress } from "./addresses"; +import { decodeBech32Pubkey } from "./encoding"; +import { MultisigThresholdPubkey } from "./pubkeys"; + +describe("addresses", () => { + describe("pubkeyToRawAddress", () => { + it("works for Secp256k1", () => { + const pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + expect(pubkeyToRawAddress(pubkey)).toEqual( + Bech32.decode("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r").data, + ); + }); + + it("works for Ed25519", () => { + const pubkey = { + type: "tendermint/PubKeyEd25519", + value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), + }; + expect(pubkeyToRawAddress(pubkey)).toEqual( + Bech32.decode("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz").data, + ); + }); + + it("works for multisig", () => { + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + ); + + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + expect(pubkeyToRawAddress(testgroup1)).toEqual(fromHex("0892a77fab2fa7e192c3b7b2741e6682f3abb72f")); + }); + }); + + describe("pubkeyToAddress", () => { + it("works for Secp256k1", () => { + const prefix = "cosmos"; + const pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r"); + }); + + it("works for Ed25519", () => { + const prefix = "cosmos"; + const pubkey = { + type: "tendermint/PubKeyEd25519", + value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), + }; + expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz"); + }); + + it("works for multisig", () => { + const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + ); + const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + ); + const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + ); + + const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, + }; + expect(pubkeyToAddress(testgroup1, "wasm")).toEqual("wasm1pzf2wlat97n7rykrk7e8g8nxste6hde0r8jqsy"); + }); + }); +}); diff --git a/packages/amino/src/addresses.ts b/packages/amino/src/addresses.ts new file mode 100644 index 00000000..133f8f8b --- /dev/null +++ b/packages/amino/src/addresses.ts @@ -0,0 +1,38 @@ +// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography + +import { ripemd160, sha256 } from "@cosmjs/crypto"; +import { Bech32, fromBase64 } from "@cosmjs/encoding"; + +import { encodeAminoPubkey } from "./encoding"; +import { isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, Pubkey } from "./pubkeys"; + +export function rawSecp256k1PubkeyToRawAddress(pubkeyData: Uint8Array): Uint8Array { + if (pubkeyData.length !== 33) { + throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyData.length}`); + } + return ripemd160(sha256(pubkeyData)); +} + +// For secp256k1 this assumes we already have a compressed pubkey. +export function pubkeyToRawAddress(pubkey: Pubkey): Uint8Array { + if (isSecp256k1Pubkey(pubkey)) { + const pubkeyData = fromBase64(pubkey.value); + return rawSecp256k1PubkeyToRawAddress(pubkeyData); + } else if (isEd25519Pubkey(pubkey)) { + const pubkeyData = fromBase64(pubkey.value); + if (pubkeyData.length !== 32) { + throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyData.length}`); + } + return sha256(pubkeyData).slice(0, 20); + } else if (isMultisigThresholdPubkey(pubkey)) { + // https://github.com/tendermint/tendermint/blob/38b401657e4ad7a7eeb3c30a3cbf512037df3740/crypto/multisig/threshold_pubkey.go#L71-L74 + const pubkeyData = encodeAminoPubkey(pubkey); + return sha256(pubkeyData).slice(0, 20); + } else { + throw new Error("Unsupported public key type"); + } +} + +export function pubkeyToAddress(pubkey: Pubkey, prefix: string): string { + return Bech32.encode(prefix, pubkeyToRawAddress(pubkey)); +} diff --git a/packages/amino/src/encoding.spec.ts b/packages/amino/src/encoding.spec.ts new file mode 100644 index 00000000..e497d84c --- /dev/null +++ b/packages/amino/src/encoding.spec.ts @@ -0,0 +1,163 @@ +import { Bech32, fromBase64 } from "@cosmjs/encoding"; + +import { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./encoding"; +import { Pubkey } from "./pubkeys"; +import { + testgroup1, + testgroup1Address, + testgroup2, + testgroup2Address, + testgroup3, + testgroup3Address, + testgroup4, + testgroup4Address, +} from "./testutils.spec"; + +describe("encoding", () => { + describe("encodeSecp256k1Pubkey", () => { + it("encodes a compressed pubkey", () => { + const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); + expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }); + }); + + it("throws for uncompressed public keys", () => { + const pubkey = fromBase64( + "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", + ); + expect(() => encodeSecp256k1Pubkey(pubkey)).toThrowError(/public key must be compressed secp256k1/i); + }); + }); + + 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=", + }); + }); + + it("works for sr25519", () => { + pending("No test data available"); + }); + }); + + describe("decodeBech32Pubkey", () => { + it("works", () => { + expect( + decodeBech32Pubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), + ).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }); + }); + + it("works for enigma pubkey", () => { + expect( + decodeBech32Pubkey("enigmapub1addwnpepqw5k9p439nw0zpg2aundx4umwx4nw233z5prpjqjv5anl5grmnchzp2xwvv"), + ).toEqual({ + type: "tendermint/PubKeySecp256k1", + 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", () => { + it("works for secp256k1", () => { + const pubkey: Pubkey = { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }; + expect(encodeBech32Pubkey(pubkey, "cosmospub")).toEqual( + "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", + ); + }); + + it("works for multisig", () => { + const expected1 = Bech32.decode(testgroup1Address).data; + expect(encodeAminoPubkey(testgroup1)).toEqual(expected1); + + const expected2 = Bech32.decode(testgroup2Address).data; + expect(encodeAminoPubkey(testgroup2)).toEqual(expected2); + + const expected3 = Bech32.decode(testgroup3Address).data; + expect(encodeAminoPubkey(testgroup3)).toEqual(expected3); + + const expected4 = Bech32.decode(testgroup4Address).data; + expect(encodeAminoPubkey(testgroup4)).toEqual(expected4); + }); + }); +}); diff --git a/packages/amino/src/encoding.ts b/packages/amino/src/encoding.ts new file mode 100644 index 00000000..79b0e4ab --- /dev/null +++ b/packages/amino/src/encoding.ts @@ -0,0 +1,127 @@ +import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; +import { arrayContentStartsWith } from "@cosmjs/utils"; + +import { + isEd25519Pubkey, + isMultisigThresholdPubkey, + isSecp256k1Pubkey, + Pubkey, + pubkeyType, + Secp256k1Pubkey, +} from "./pubkeys"; + +export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Secp256k1Pubkey { + if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) { + throw new Error("Public key must be compressed secp256k1, i.e. 33 bytes starting with 0x02 or 0x03"); + } + return { + type: pubkeyType.secp256k1, + value: toBase64(pubkey), + }; +} + +// As discussed in https://github.com/binance-chain/javascript-sdk/issues/163 +// Prefixes listed here: https://github.com/tendermint/tendermint/blob/d419fffe18531317c28c29a292ad7d253f6cafdf/docs/spec/blockchain/encoding.md#public-key-cryptography +// Last bytes is varint-encoded length prefix +const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae987" + "21" /* fixed length */); +const pubkeyAminoPrefixEd25519 = fromHex("1624de64" + "20" /* fixed length */); +const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005" + "20" /* fixed length */); +/** See https://github.com/tendermint/tendermint/commit/38b401657e4ad7a7eeb3c30a3cbf512037df3740 */ +const pubkeyAminoPrefixMultisigThreshold = fromHex("22c1f7e2" /* variable length not included */); + +/** + * Decodes a pubkey in the Amino binary format to a type/value object. + */ +export function decodeAminoPubkey(data: Uint8Array): Pubkey { + if (arrayContentStartsWith(data, pubkeyAminoPrefixSecp256k1)) { + const rest = data.slice(pubkeyAminoPrefixSecp256k1.length); + if (rest.length !== 33) { + throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); + } + return { + type: pubkeyType.secp256k1, + value: toBase64(rest), + }; + } else if (arrayContentStartsWith(data, pubkeyAminoPrefixEd25519)) { + const rest = data.slice(pubkeyAminoPrefixEd25519.length); + if (rest.length !== 32) { + throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey)."); + } + return { + type: pubkeyType.ed25519, + value: toBase64(rest), + }; + } else if (arrayContentStartsWith(data, pubkeyAminoPrefixSr25519)) { + const rest = data.slice(pubkeyAminoPrefixSr25519.length); + if (rest.length !== 32) { + throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey)."); + } + return { + type: pubkeyType.sr25519, + value: toBase64(rest), + }; + } else { + throw new Error("Unsupported public key type. Amino data starts with: " + toHex(data.slice(0, 5))); + } +} + +/** + * 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); +} + +/** + * Uvarint encoder for Amino. This is the same encoding as `binary.PutUvarint` from the Go + * standard library. + * + * @see https://github.com/tendermint/go-amino/blob/8e779b71f40d175/encoder.go#L77-L85 + */ +function encodeUvarint(value: number | string): number[] { + const checked = Uint53.fromString(value.toString()).toNumber(); + if (checked > 127) { + throw new Error( + "Encoding numbers > 127 is not supported here. Please tell those lazy CosmJS maintainers to port the binary.PutUvarint implementation from the Go standard library and write some tests.", + ); + } + return [checked]; +} + +/** + * Encodes a public key to binary Amino. + */ +export function encodeAminoPubkey(pubkey: Pubkey): Uint8Array { + if (isMultisigThresholdPubkey(pubkey)) { + const out = Array.from(pubkeyAminoPrefixMultisigThreshold); + out.push(0x08); // TODO: What is this? + out.push(...encodeUvarint(pubkey.value.threshold)); + for (const pubkeyData of pubkey.value.pubkeys.map((p) => encodeAminoPubkey(p))) { + out.push(0x12); // TODO: What is this? + out.push(...encodeUvarint(pubkeyData.length)); + out.push(...pubkeyData); + } + return new Uint8Array(out); + } else if (isEd25519Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixEd25519, ...fromBase64(pubkey.value)]); + } else if (isSecp256k1Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixSecp256k1, ...fromBase64(pubkey.value)]); + } else { + throw new Error("Unsupported pubkey type"); + } +} + +/** + * 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/amino/src/index.ts b/packages/amino/src/index.ts new file mode 100644 index 00000000..28b4d9e5 --- /dev/null +++ b/packages/amino/src/index.ts @@ -0,0 +1,21 @@ +export { pubkeyToAddress, pubkeyToRawAddress, rawSecp256k1PubkeyToRawAddress } from "./addresses"; +export { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, +} from "./encoding"; +export { + MultisigThresholdPubkey, + Pubkey, + Ed25519Pubkey, + Secp256k1Pubkey, + SinglePubkey, + isMultisigThresholdPubkey, + isEd25519Pubkey, + isSecp256k1Pubkey, + isSinglePubkey, + pubkeyType, +} from "./pubkeys"; +export { createMultisigThresholdPubkey } from "./multisig"; diff --git a/packages/amino/src/multisig.spec.ts b/packages/amino/src/multisig.spec.ts new file mode 100644 index 00000000..42bd1869 --- /dev/null +++ b/packages/amino/src/multisig.spec.ts @@ -0,0 +1,84 @@ +import { compareArrays, createMultisigThresholdPubkey } from "./multisig"; +import { test1, test2, test3, testgroup1, testgroup2, testgroup3, testgroup4 } from "./testutils.spec"; + +describe("multisig", () => { + describe("compareArrays", () => { + it("return 0 for equal arrays", () => { + expect(compareArrays(new Uint8Array([]), new Uint8Array([]))).toEqual(0); + expect(compareArrays(new Uint8Array([1]), new Uint8Array([1]))).toEqual(0); + expect(compareArrays(new Uint8Array([3, 2, 1]), new Uint8Array([3, 2, 1]))).toEqual(0); + }); + + it("return > 0 for left > right", () => { + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5, 4]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 4, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([4, 5, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5, 5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([5]))).toBeGreaterThan(0); + expect(compareArrays(new Uint8Array([5, 5, 5]), new Uint8Array([]))).toBeGreaterThan(0); + + // left or right precedence + expect(compareArrays(new Uint8Array([5, 5, 4]), new Uint8Array([4, 5, 5]))).toBeGreaterThan(0); + + // magnitude is more important than length + expect(compareArrays(new Uint8Array([6]), new Uint8Array([5, 5]))).toBeGreaterThan(0); + }); + + it("return < 0 for left < right", () => { + expect(compareArrays(new Uint8Array([5, 5, 4]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5, 4, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([4, 5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5, 5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([5]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + expect(compareArrays(new Uint8Array([]), new Uint8Array([5, 5, 5]))).toBeLessThan(0); + + // left or right precedence + expect(compareArrays(new Uint8Array([4, 5, 5]), new Uint8Array([5, 5, 4]))).toBeLessThan(0); + + // magnitude is more important than length + expect(compareArrays(new Uint8Array([5, 5]), new Uint8Array([6]))).toBeLessThan(0); + }); + + it("can be used with sort", () => { + const values = [ + new Uint8Array([2]), + new Uint8Array([1]), + new Uint8Array([2, 5]), + new Uint8Array([3]), + new Uint8Array([]), + ].sort(compareArrays); + expect(values).toEqual([ + new Uint8Array([]), + new Uint8Array([1]), + new Uint8Array([2]), + new Uint8Array([2, 5]), + new Uint8Array([3]), + ]); + }); + }); + + describe("MultisigThresholdPubkey", () => { + it("works with sorting", () => { + expect(createMultisigThresholdPubkey([test1, test2, test3], 2)).toEqual(testgroup1); + expect(createMultisigThresholdPubkey([test1, test2, test3], 1)).toEqual(testgroup2); + expect(createMultisigThresholdPubkey([test3, test1], 2)).toEqual(testgroup3); + + expect(createMultisigThresholdPubkey([test1, test2, test3], 2, false)).toEqual(testgroup1); + expect(createMultisigThresholdPubkey([test1, test2, test3], 1, false)).toEqual(testgroup2); + expect(createMultisigThresholdPubkey([test3, test1], 2, false)).toEqual(testgroup3); + }); + + it("works with nosort", () => { + expect(createMultisigThresholdPubkey([test3, test1], 2, true)).toEqual(testgroup4); + }); + + it("throws for threshold larger than number of keys", () => { + expect(() => createMultisigThresholdPubkey([test1, test2, test3], 4)).toThrowError( + /threshold k = 4 exceeds number of keys n = 3/i, + ); + expect(() => createMultisigThresholdPubkey([test1, test2, test3], 75)).toThrowError( + /threshold k = 75 exceeds number of keys n = 3/i, + ); + }); + }); +}); diff --git a/packages/amino/src/multisig.ts b/packages/amino/src/multisig.ts new file mode 100644 index 00000000..7e8ecc5a --- /dev/null +++ b/packages/amino/src/multisig.ts @@ -0,0 +1,45 @@ +import { toHex } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; + +import { pubkeyToRawAddress } from "./addresses"; +import { MultisigThresholdPubkey, SinglePubkey } from "./pubkeys"; + +/** + * Compare arrays lexicographically. + * + * Returns value < 0 if `a < b`. + * Returns value > 0 if `a > b`. + * Returns 0 if `a === b`. + */ +export function compareArrays(a: Uint8Array, b: Uint8Array): number { + const aHex = toHex(a); + const bHex = toHex(b); + return aHex === bHex ? 0 : aHex < bHex ? -1 : 1; +} + +export function createMultisigThresholdPubkey( + pubkeys: readonly SinglePubkey[], + threshold: number, + nosort = false, +): MultisigThresholdPubkey { + const uintThreshold = new Uint53(threshold); + if (uintThreshold.toNumber() > pubkeys.length) { + throw new Error(`Threshold k = ${uintThreshold.toNumber()} exceeds number of keys n = ${pubkeys.length}`); + } + + const outPubkeys = nosort + ? pubkeys + : Array.from(pubkeys).sort((lhs, rhs) => { + // https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174 + const addressLhs = pubkeyToRawAddress(lhs); + const addressRhs = pubkeyToRawAddress(rhs); + return compareArrays(addressLhs, addressRhs); + }); + return { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: uintThreshold.toString(), + pubkeys: outPubkeys, + }, + }; +} diff --git a/packages/amino/src/pubkeys.spec.ts b/packages/amino/src/pubkeys.spec.ts new file mode 100644 index 00000000..ec47bdcb --- /dev/null +++ b/packages/amino/src/pubkeys.spec.ts @@ -0,0 +1,52 @@ +import { isMultisigThresholdPubkey, isSinglePubkey } from "./pubkeys"; + +describe("pubkeys", () => { + const pubkeyEd25519 = { + type: "tendermint/PubKeyEd25519", + value: "YZHlYxP5R6olj3Tj3f7VgkQE5VaOvv9G0jKATqdQsqI=", + }; + const pubkeySecp256k1 = { + type: "tendermint/PubKeySecp256k1", + value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", + }; + const pubkeyMultisigThreshold = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "3", + pubkeys: [ + { + type: "tendermint/PubKeySecp256k1", + value: "A4KZH7VSRwW/6RTExROivRYKsQP63LnGcBlXFo+eKGpQ", + }, + { + type: "tendermint/PubKeySecp256k1", + value: "A8/Cq4VigOnDgl6RSdcx97fjrdCo/qwAX6C34n7ZDZLs", + }, + { + type: "tendermint/PubKeySecp256k1", + value: "ApKgZuwy03xgdRnXqG6yEHATomsWDOPacy7nbpsuUCSS", + }, + { + type: "tendermint/PubKeySecp256k1", + value: "Aptm8E3WSSFS0RTAIUW+bLi/slYnTEE+h4qPTG28CHfq", + }, + ], + }, + }; + + describe("isSinglePubkey", () => { + it("works", () => { + expect(isSinglePubkey(pubkeyEd25519)).toEqual(true); + expect(isSinglePubkey(pubkeySecp256k1)).toEqual(true); + expect(isSinglePubkey(pubkeyMultisigThreshold)).toEqual(false); + }); + }); + + describe("isMultisigThresholdPubkey", () => { + it("works", () => { + expect(isMultisigThresholdPubkey(pubkeyEd25519)).toEqual(false); + expect(isMultisigThresholdPubkey(pubkeySecp256k1)).toEqual(false); + expect(isMultisigThresholdPubkey(pubkeyMultisigThreshold)).toEqual(true); + }); + }); +}); diff --git a/packages/amino/src/pubkeys.ts b/packages/amino/src/pubkeys.ts new file mode 100644 index 00000000..d61bfa3c --- /dev/null +++ b/packages/amino/src/pubkeys.ts @@ -0,0 +1,71 @@ +export interface Pubkey { + // 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/cosmos/cosmjs/pull/44#pullrequestreview-353280504 + readonly type: string; + readonly value: any; +} + +export interface Ed25519Pubkey extends SinglePubkey { + readonly type: "tendermint/PubKeyEd25519"; + readonly value: string; +} + +export function isEd25519Pubkey(pubkey: Pubkey): pubkey is Ed25519Pubkey { + return (pubkey as Ed25519Pubkey).type === "tendermint/PubKeyEd25519"; +} + +export interface Secp256k1Pubkey extends SinglePubkey { + readonly type: "tendermint/PubKeySecp256k1"; + readonly value: string; +} + +export function isSecp256k1Pubkey(pubkey: Pubkey): pubkey is Secp256k1Pubkey { + return (pubkey as Secp256k1Pubkey).type === "tendermint/PubKeySecp256k1"; +} + +export const pubkeyType = { + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ + secp256k1: "tendermint/PubKeySecp256k1" as const, + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */ + ed25519: "tendermint/PubKeyEd25519" as const, + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ + sr25519: "tendermint/PubKeySr25519" as const, + multisigThreshold: "tendermint/PubKeyMultisigThreshold" as const, +}; + +/** + * A pubkey which contains the data directly without further nesting. + * + * You can think of this as a non-multisig pubkey. + */ +export interface SinglePubkey extends Pubkey { + // 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/cosmos/cosmjs/pull/44#pullrequestreview-353280504 + readonly type: string; + /** + * The base64 encoding of the Amino binary encoded pubkey. + * + * Note: if type is Secp256k1, this must contain a 33 bytes compressed pubkey. + */ + readonly value: string; +} + +export function isSinglePubkey(pubkey: Pubkey): pubkey is SinglePubkey { + const singPubkeyTypes: string[] = [pubkeyType.ed25519, pubkeyType.secp256k1, pubkeyType.sr25519]; + return singPubkeyTypes.includes(pubkey.type); +} + +export interface MultisigThresholdPubkey extends Pubkey { + readonly type: "tendermint/PubKeyMultisigThreshold"; + readonly value: { + /** A string-encoded integer */ + readonly threshold: string; + readonly pubkeys: readonly SinglePubkey[]; + }; +} + +export function isMultisigThresholdPubkey(pubkey: Pubkey): pubkey is MultisigThresholdPubkey { + return (pubkey as MultisigThresholdPubkey).type === "tendermint/PubKeyMultisigThreshold"; +} diff --git a/packages/amino/src/testutils.spec.ts b/packages/amino/src/testutils.spec.ts new file mode 100644 index 00000000..36cbc26d --- /dev/null +++ b/packages/amino/src/testutils.spec.ts @@ -0,0 +1,73 @@ +import { decodeBech32Pubkey } from "./encoding"; +import { MultisigThresholdPubkey } from "./pubkeys"; + +// ./build/wasmd keys add test1 +// ./build/wasmd keys add test2 +// ./build/wasmd keys add test3 +// ./build/wasmd keys add testgroup1 --multisig=test1,test2,test3 --multisig-threshold 2 +// ./build/wasmd keys add testgroup2 --multisig=test1,test2,test3 --multisig-threshold 1 +// # By default pubkeys are sorted by its address data (https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/client/keys/add.go#L172-L174) +// ./build/wasmd keys add testgroup3 --multisig=test3,test1 --multisig-threshold 2 +// ./build/wasmd keys add testgroup4 --multisig=test3,test1 --nosort --multisig-threshold 2 + +export const test1 = decodeBech32Pubkey( + "wasmpub1addwnpepqwxttx8w2sfs6d8cuzqcuau84grp8xsw95qzdjkmvc44tnckskdxw3zw2km", + // pubkey data: eb5ae98721038cb598ee54130d34f8e0818e7787aa06139a0e2d0026cadb662b55cf16859a67 + // address: wasm1jq59w7y34msq69g4w3zvq6d5h3stcajd8g62xm + // address data: 9028577891aee00d15157444c069b4bc60bc764d +); +export const test2 = decodeBech32Pubkey( + "wasmpub1addwnpepq2gx7x7e29kge5a4ycunytyqr0u8ynql5h583s8r9wdads9m3v8ks6y0nhc", + // pubkey data: eb5ae9872102906f1bd9516c8cd3b52639322c801bf8724c1fa5e878c0e32b9bd6c0bb8b0f68 + // address: wasm146e52j6zphxw8m67cz8860ad5uju892cqmawsg + // address data: aeb3454b420dcce3ef5ec08e7d3fada725c39558 +); +export const test3 = decodeBech32Pubkey( + "wasmpub1addwnpepq0xfx5vavxmgdkn0p6x0l9p3udttghu3qcldd7ql08wa3xy93qq0xuzvtxc", + // pubkey data: eb5ae9872103cc93519d61b686da6f0e8cff9431e356b45f91063ed6f81f79ddd898858800f3 + // address: wasm1a6uxr25mw8qg8zz3l2avsdjsveh4yg9sw7h5np + // address data: eeb861aa9b71c0838851fabac83650666f5220b0 +); + +// 2/3 multisig +export const testgroup1: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test2, test3], + }, +}; +export const testgroup1Address = + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7v7aysdd"; + +export const testgroup2: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "1", + pubkeys: [test1, test2, test3], + }, +}; +export const testgroup2Address = + "wasmpub1ytql0csgqyfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq5sdudaj5tv3nfm2f3exgkgqxlcwfxplf0g0rqwx2um6mqthzc0dqfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vc4ejke"; + +// 2/2 multisig +export const testgroup3: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test1, test3], + }, +}; +export const testgroup3Address = + "wasmpub1ytql0csgqgfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vufzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vzjhugu"; + +// 2/2 multisig with custom sorting +export const testgroup4: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: "2", + pubkeys: [test3, test1], + }, +}; +export const testgroup4Address = + "wasmpub1ytql0csgqgfzd666axrjzq7vjdge6cdksmdx7r5vl72rrc6kk30ezp376mup77wamzvgtzqq7vfzd666axrjzquvkkvwu4qnp5603cyp3emc02sxzwdqutgqym9dke3t2h83dpv6vujvg56k"; diff --git a/packages/amino/tsconfig.json b/packages/amino/tsconfig.json new file mode 100644 index 00000000..df66add1 --- /dev/null +++ b/packages/amino/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/amino/typedoc.js b/packages/amino/typedoc.js new file mode 100644 index 00000000..ffe4be64 --- /dev/null +++ b/packages/amino/typedoc.js @@ -0,0 +1,11 @@ +const packageJson = require("./package.json"); + +module.exports = { + entryPoints: ["./src"], + out: "docs", + exclude: "**/*.spec.ts", + name: `${packageJson.name} Documentation`, + readme: "README.md", + excludeExternals: true, + excludePrivate: true, +}; diff --git a/packages/amino/webpack.web.config.js b/packages/amino/webpack.web.config.js new file mode 100644 index 00000000..9d5836a8 --- /dev/null +++ b/packages/amino/webpack.web.config.js @@ -0,0 +1,17 @@ +const glob = require("glob"); +const path = require("path"); + +const target = "web"; +const distdir = path.join(__dirname, "dist", "web"); + +module.exports = [ + { + // bundle used for Karma tests + target: target, + entry: glob.sync("./build/**/*.spec.js"), + output: { + path: distdir, + filename: "tests.js", + }, + }, +]; diff --git a/packages/cli/examples/multisig_address.ts b/packages/cli/examples/multisig_address.ts new file mode 100644 index 00000000..94c37c1a --- /dev/null +++ b/packages/cli/examples/multisig_address.ts @@ -0,0 +1,30 @@ +import { MultisigThresholdPubkey, pubkeyToAddress } from "@cosmjs/amino"; + +// https://github.com/cosmos/cosmjs/issues/673#issuecomment-779847238 +const multisigPubkey: MultisigThresholdPubkey = { + "type": "tendermint/PubKeyMultisigThreshold", + "value": { + "threshold": "3", + "pubkeys": [ + { + "type": "tendermint/PubKeySecp256k1", + "value": "A4KZH7VSRwW/6RTExROivRYKsQP63LnGcBlXFo+eKGpQ" + }, + { + "type": "tendermint/PubKeySecp256k1", + "value": "A8/Cq4VigOnDgl6RSdcx97fjrdCo/qwAX6C34n7ZDZLs" + }, + { + "type": "tendermint/PubKeySecp256k1", + "value": "ApKgZuwy03xgdRnXqG6yEHATomsWDOPacy7nbpsuUCSS" + }, + { + "type": "tendermint/PubKeySecp256k1", + "value": "Aptm8E3WSSFS0RTAIUW+bLi/slYnTEE+h4qPTG28CHfq" + } + ] + } +}; + +const address = pubkeyToAddress(multisigPubkey, "cosmos"); +console.log(address); diff --git a/packages/cli/package.json b/packages/cli/package.json index 5949e477..7405f013 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -39,6 +39,7 @@ "!**/testdata/" ], "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/cosmwasm-launchpad": "^0.25.0-alpha.0", "@cosmjs/cosmwasm-stargate": "^0.25.0-alpha.0", "@cosmjs/crypto": "^0.25.0-alpha.0", diff --git a/packages/cli/run_examples.sh b/packages/cli/run_examples.sh index fde36611..8ec0e10e 100755 --- a/packages/cli/run_examples.sh +++ b/packages/cli/run_examples.sh @@ -13,6 +13,7 @@ cosmwasm-cli --init examples/generate_address.ts --code "process.exit(0)" cosmwasm-cli --init examples/helpers.ts --code "process.exit(0)" cosmwasm-cli --init examples/local_faucet.ts --code "process.exit(0)" cosmwasm-cli --init examples/mask.ts --code "process.exit(0)" +cosmwasm-cli --init examples/multisig_address.ts --code "process.exit(0)" if [ -n "${SIMAPP_ENABLED:-}" ]; then cosmwasm-cli --init examples/stargate.ts --code "process.exit(0)" fi diff --git a/packages/cosmwasm-stargate/package.json b/packages/cosmwasm-stargate/package.json index 8261f247..fa4945e3 100644 --- a/packages/cosmwasm-stargate/package.json +++ b/packages/cosmwasm-stargate/package.json @@ -42,6 +42,7 @@ "postdefine-proto": "prettier --write \"src/codec/**/*.ts\"" }, "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/cosmwasm-launchpad": "^0.25.0-alpha.0", "@cosmjs/crypto": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index e4d12d71..7b008c5e 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeSecp256k1Pubkey } from "@cosmjs/amino"; import { ChangeAdminResult, CosmWasmFeeTable, @@ -16,7 +17,6 @@ import { buildFeeTable, Coin, CosmosFeeTable, - encodeSecp256k1Pubkey, GasLimits, GasPrice, logs, diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index 0c8a4bfa..d6ed8030 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -39,6 +39,7 @@ "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" }, "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/crypto": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", "@cosmjs/math": "^0.25.0-alpha.0", diff --git a/packages/launchpad/src/address.spec.ts b/packages/launchpad/src/address.spec.ts deleted file mode 100644 index f5e3b627..00000000 --- a/packages/launchpad/src/address.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { fromHex, toBase64 } from "@cosmjs/encoding"; - -import { pubkeyToAddress } from "./address"; - -describe("address", () => { - describe("pubkeyToAddress", () => { - it("works for Secp256k1 compressed", () => { - const prefix = "cosmos"; - const pubkey = { - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }; - expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r"); - }); - - it("works for Ed25519", () => { - const prefix = "cosmos"; - const pubkey = { - type: "tendermint/PubKeyEd25519", - value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")), - }; - expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz"); - }); - }); -}); diff --git a/packages/launchpad/src/address.ts b/packages/launchpad/src/address.ts deleted file mode 100644 index 87fede26..00000000 --- a/packages/launchpad/src/address.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ripemd160, sha256 } from "@cosmjs/crypto"; -import { Bech32, fromBase64 } from "@cosmjs/encoding"; - -import { PubKey, pubkeyType } from "./types"; - -export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string { - if (pubkeyRaw.length !== 33) { - throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyRaw.length}`); - } - const hash1 = sha256(pubkeyRaw); - const hash2 = ripemd160(hash1); - return Bech32.encode(prefix, hash2); -} - -// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography -// This assumes we already have a cosmos-compressed pubkey -export function pubkeyToAddress(pubkey: PubKey, prefix: string): string { - const pubkeyBytes = fromBase64(pubkey.value); - switch (pubkey.type) { - case pubkeyType.secp256k1: { - return rawSecp256k1PubkeyToAddress(pubkeyBytes, prefix); - } - case pubkeyType.ed25519: { - if (pubkeyBytes.length !== 32) { - throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyBytes.length}`); - } - const hash = sha256(pubkeyBytes); - return Bech32.encode(prefix, hash.slice(0, 20)); - } - case pubkeyType.sr25519: { - if (pubkeyBytes.length !== 32) { - throw new Error(`Invalid Sr25519 pubkey length: ${pubkeyBytes.length}`); - } - const hash = sha256(pubkeyBytes); - return Bech32.encode(prefix, hash.slice(0, 20)); - } - default: - throw new Error("Unrecognized public key algorithm"); - } -} diff --git a/packages/launchpad/src/cosmosclient.ts b/packages/launchpad/src/cosmosclient.ts index 79194bc9..9bb2764e 100644 --- a/packages/launchpad/src/cosmosclient.ts +++ b/packages/launchpad/src/cosmosclient.ts @@ -1,3 +1,4 @@ +import { Pubkey } from "@cosmjs/amino"; import { sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; @@ -13,7 +14,6 @@ import { } from "./lcdapi"; import { Log, parseLogs } from "./logs"; import { StdTx, WrappedStdTx } from "./tx"; -import { PubKey } from "./types"; export interface GetSequenceResult { readonly accountNumber: number; @@ -24,7 +24,7 @@ export interface Account { /** Bech32 account address */ readonly address: string; readonly balance: readonly Coin[]; - readonly pubkey: PubKey | undefined; + readonly pubkey: Pubkey | undefined; readonly accountNumber: number; readonly sequence: number; } diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index dc7a11d0..14db51fa 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -1,7 +1,20 @@ +// Re-exports for backwards compatibility +export { + decodeAminoPubkey, + decodeBech32Pubkey, + encodeAminoPubkey, + encodeBech32Pubkey, + encodeSecp256k1Pubkey, + pubkeyToAddress, + pubkeyType, +} from "@cosmjs/amino"; +import { SinglePubkey } from "@cosmjs/amino"; +/** @deprecated PubKey is deprecated. Use `SinglePubkey` or the more general `Pubkey` from `@cosmjs/amino`. */ +export type PubKey = SinglePubkey; + import * as logs from "./logs"; export { logs }; -export { pubkeyToAddress, rawSecp256k1PubkeyToAddress } from "./address"; export { Coin, coin, coins, parseCoins } from "./coins"; export { @@ -125,19 +138,12 @@ export { MsgWithdrawValidatorCommission, } from "./msgs"; export { makeCosmoshubPath } from "./paths"; -export { - decodeAminoPubkey, - decodeBech32Pubkey, - encodeAminoPubkey, - encodeBech32Pubkey, - encodeSecp256k1Pubkey, -} from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { AccountData, Algo, AminoSignResponse, OfflineSigner } from "./signer"; export { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, isWrappedStdTx, makeStdTx, CosmosSdkTx, StdTx, WrappedStdTx, WrappedTx } from "./tx"; -export { pubkeyType, PubKey, StdFee, StdSignature } from "./types"; +export { StdFee, StdSignature } from "./types"; export { executeKdf, KdfConfiguration } from "./wallet"; export { extractKdfConfiguration, Secp256k1HdWallet } from "./secp256k1hdwallet"; export { Secp256k1Wallet } from "./secp256k1wallet"; diff --git a/packages/launchpad/src/lcdapi/auth.ts b/packages/launchpad/src/lcdapi/auth.ts index 6b316466..947ff1da 100644 --- a/packages/launchpad/src/lcdapi/auth.ts +++ b/packages/launchpad/src/lcdapi/auth.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { Pubkey } from "@cosmjs/amino"; + import { Coin } from "../coins"; -import { PubKey } from "../types"; import { LcdClient } from "./lcdclient"; /** @@ -25,7 +26,7 @@ export interface BaseAccount { * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 */ - readonly public_key: string | PubKey | null; + readonly public_key: string | Pubkey | null; /** * The account number assigned by the blockchain. * diff --git a/packages/launchpad/src/lcdapi/utils.spec.ts b/packages/launchpad/src/lcdapi/utils.spec.ts index 27b370a9..646eee1c 100644 --- a/packages/launchpad/src/lcdapi/utils.spec.ts +++ b/packages/launchpad/src/lcdapi/utils.spec.ts @@ -1,4 +1,5 @@ -import { PubKey } from "../types"; +import { Pubkey } from "@cosmjs/amino"; + import { normalizePubkey, uint64ToNumber, uint64ToString } from "./utils"; describe("utils", () => { @@ -84,7 +85,7 @@ describe("utils", () => { }); it("passes PubKey unchanged", () => { - const original: PubKey = { + const original: Pubkey = { type: "tendermint/PubKeySecp256k1", value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", }; diff --git a/packages/launchpad/src/lcdapi/utils.ts b/packages/launchpad/src/lcdapi/utils.ts index 5f797746..558033dc 100644 --- a/packages/launchpad/src/lcdapi/utils.ts +++ b/packages/launchpad/src/lcdapi/utils.ts @@ -1,8 +1,6 @@ +import { decodeBech32Pubkey, Pubkey } from "@cosmjs/amino"; import { Uint64 } from "@cosmjs/math"; -import { decodeBech32Pubkey } from "../pubkey"; -import { PubKey } from "../types"; - /** * Converts an integer expressed as number or string to a number. * Throws if input is not a valid uint64 or if the value exceeds MAX_SAFE_INTEGER. @@ -31,7 +29,7 @@ export function uint64ToString(input: number | string): string { * * Returns null when unset. */ -export function normalizePubkey(input: string | PubKey | null): PubKey | null { +export function normalizePubkey(input: string | Pubkey | null): Pubkey | null { if (!input) return null; if (typeof input === "string") return decodeBech32Pubkey(input); return input; diff --git a/packages/launchpad/src/pubkey.spec.ts b/packages/launchpad/src/pubkey.spec.ts index 9576239b..e69de29b 100644 --- a/packages/launchpad/src/pubkey.spec.ts +++ b/packages/launchpad/src/pubkey.spec.ts @@ -1,135 +0,0 @@ -import { Bech32, fromBase64 } from "@cosmjs/encoding"; - -import { - decodeAminoPubkey, - decodeBech32Pubkey, - encodeAminoPubkey, - encodeBech32Pubkey, - encodeSecp256k1Pubkey, -} from "./pubkey"; -import { PubKey } from "./types"; - -describe("pubkey", () => { - describe("encodeSecp256k1Pubkey", () => { - it("encodes a compresed pubkey", () => { - const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"); - expect(encodeSecp256k1Pubkey(pubkey)).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", - }); - }); - - it("throws for uncompressed public keys", () => { - const pubkey = fromBase64( - "BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=", - ); - expect(() => encodeSecp256k1Pubkey(pubkey)).toThrowError(/public key must be compressed secp256k1/i); - }); - }); - - 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( - decodeBech32Pubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), - ).toEqual({ - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }); - }); - - it("works for enigma pubkey", () => { - expect( - decodeBech32Pubkey("enigmapub1addwnpepqw5k9p439nw0zpg2aundx4umwx4nw233z5prpjqjv5anl5grmnchzp2xwvv"), - ).toEqual({ - type: "tendermint/PubKeySecp256k1", - 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", () => { - it("works for secp256k1", () => { - const pubkey: PubKey = { - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }; - expect(encodeBech32Pubkey(pubkey, "cosmospub")).toEqual( - "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 bebc3c62..e69de29b 100644 --- a/packages/launchpad/src/pubkey.ts +++ b/packages/launchpad/src/pubkey.ts @@ -1,97 +0,0 @@ -import { Bech32, fromBase64, fromHex, toBase64, toHex } from "@cosmjs/encoding"; -import { arrayContentEquals } from "@cosmjs/utils"; - -import { PubKey, pubkeyType } from "./types"; - -export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey { - if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) { - throw new Error("Public key must be compressed secp256k1, i.e. 33 bytes starting with 0x02 or 0x03"); - } - return { - type: pubkeyType.secp256k1, - value: toBase64(pubkey), - }; -} - -// As discussed in https://github.com/binance-chain/javascript-sdk/issues/163 -// Prefixes listed here: https://github.com/tendermint/tendermint/blob/d419fffe18531317c28c29a292ad7d253f6cafdf/docs/spec/blockchain/encoding.md#public-key-cryptography -// Last bytes is varint-encoded length prefix -const pubkeyAminoPrefixSecp256k1 = fromHex("eb5ae98721"); -const pubkeyAminoPrefixEd25519 = fromHex("1624de6420"); -const pubkeyAminoPrefixSr25519 = fromHex("0dfb1005"); -const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; - -/** - * 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 (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { - if (rest.length !== 33) { - throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); - } - return { - type: pubkeyType.secp256k1, - value: toBase64(rest), - }; - } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixEd25519)) { - if (rest.length !== 32) { - throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey)."); - } - return { - type: pubkeyType.ed25519, - value: toBase64(rest), - }; - } else if (arrayContentEquals(aminoPrefix, pubkeyAminoPrefixSr25519)) { - if (rest.length !== 32) { - throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey)."); - } - return { - type: pubkeyType.sr25519, - value: toBase64(rest), - }; - } else { - throw new Error("Unsupported Pubkey type. Amino prefix: " + toHex(aminoPrefix)); - } -} - -/** - * 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"); - } - 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/secp256k1hdwallet.ts b/packages/launchpad/src/secp256k1hdwallet.ts index cea4e63b..fa7d8b9c 100644 --- a/packages/launchpad/src/secp256k1hdwallet.ts +++ b/packages/launchpad/src/secp256k1hdwallet.ts @@ -1,3 +1,4 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, @@ -10,10 +11,9 @@ import { Slip10Curve, stringToPath, } from "@cosmjs/crypto"; -import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; +import { Bech32, fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; import { assert, isNonNullObject } from "@cosmjs/utils"; -import { rawSecp256k1PubkeyToAddress } from "./address"; import { serializeSignDoc, StdSignDoc } from "./encoding"; import { makeCosmoshubPath } from "./paths"; import { encodeSecp256k1Signature } from "./signature"; @@ -246,7 +246,7 @@ export class Secp256k1HdWallet implements OfflineSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix); + return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/launchpad/src/secp256k1wallet.ts b/packages/launchpad/src/secp256k1wallet.ts index b8e84960..94552d84 100644 --- a/packages/launchpad/src/secp256k1wallet.ts +++ b/packages/launchpad/src/secp256k1wallet.ts @@ -1,6 +1,7 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Secp256k1, Sha256 } from "@cosmjs/crypto"; +import { Bech32 } from "@cosmjs/encoding"; -import { rawSecp256k1PubkeyToAddress } from "./address"; import { serializeSignDoc, StdSignDoc } from "./encoding"; import { encodeSecp256k1Signature } from "./signature"; import { AccountData, AminoSignResponse, OfflineSigner } from "./signer"; @@ -33,7 +34,7 @@ export class Secp256k1Wallet implements OfflineSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix); + return Bech32.encode(this.prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/launchpad/src/signature.ts b/packages/launchpad/src/signature.ts index d94ad665..15f37f00 100644 --- a/packages/launchpad/src/signature.ts +++ b/packages/launchpad/src/signature.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeSecp256k1Pubkey, pubkeyType } from "@cosmjs/amino"; import { fromBase64, toBase64 } from "@cosmjs/encoding"; -import { encodeSecp256k1Pubkey } from "./pubkey"; -import { pubkeyType, StdSignature } from "./types"; +import { StdSignature } from "./types"; /** * Takes a binary pubkey and signature to create a signature object diff --git a/packages/launchpad/src/types.ts b/packages/launchpad/src/types.ts index 8f0b5565..bb674bc1 100644 --- a/packages/launchpad/src/types.ts +++ b/packages/launchpad/src/types.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { Pubkey } from "@cosmjs/amino"; + import { Coin } from "./coins"; export interface StdFee { @@ -7,25 +9,6 @@ export interface StdFee { } export interface StdSignature { - readonly pub_key: PubKey; + readonly pub_key: Pubkey; readonly signature: string; } - -export interface PubKey { - // 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/cosmos/cosmjs/pull/44#pullrequestreview-353280504 - readonly type: string; - // Value field is base64-encoded in all cases - // Note: if type is Secp256k1, this must contain a COMPRESSED pubkey - to encode from bcp/keycontrol land, you must compress it first - readonly value: string; -} - -export const pubkeyType = { - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ - secp256k1: "tendermint/PubKeySecp256k1" as const, - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */ - ed25519: "tendermint/PubKeyEd25519" as const, - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ - sr25519: "tendermint/PubKeySr25519" as const, -}; diff --git a/packages/proto-signing/package.json b/packages/proto-signing/package.json index 64355abd..806b3a8f 100644 --- a/packages/proto-signing/package.json +++ b/packages/proto-signing/package.json @@ -43,6 +43,7 @@ "postdefine-proto": "prettier --write \"src/codec/**/*.ts\"" }, "dependencies": { + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/launchpad": "^0.25.0-alpha.0", "long": "^4.0.0", "protobufjs": "~6.10.2" diff --git a/packages/proto-signing/src/directsecp256k1hdwallet.ts b/packages/proto-signing/src/directsecp256k1hdwallet.ts index bb6058d7..cfdbc8c4 100644 --- a/packages/proto-signing/src/directsecp256k1hdwallet.ts +++ b/packages/proto-signing/src/directsecp256k1hdwallet.ts @@ -1,3 +1,4 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Bip39, EnglishMnemonic, @@ -8,7 +9,8 @@ import { Slip10, Slip10Curve, } from "@cosmjs/crypto"; -import { encodeSecp256k1Signature, rawSecp256k1PubkeyToAddress } from "@cosmjs/launchpad"; +import { Bech32 } from "@cosmjs/encoding"; +import { encodeSecp256k1Signature } from "@cosmjs/launchpad"; import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx"; import { makeCosmoshubPath } from "./paths"; @@ -99,7 +101,7 @@ export class DirectSecp256k1HdWallet implements OfflineDirectSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix); + return Bech32.encode(this.accounts[0].prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/proto-signing/src/directsecp256k1wallet.ts b/packages/proto-signing/src/directsecp256k1wallet.ts index 28133f10..9207d9c6 100644 --- a/packages/proto-signing/src/directsecp256k1wallet.ts +++ b/packages/proto-signing/src/directsecp256k1wallet.ts @@ -1,5 +1,7 @@ +import { rawSecp256k1PubkeyToRawAddress } from "@cosmjs/amino"; import { Secp256k1, sha256 } from "@cosmjs/crypto"; -import { encodeSecp256k1Signature, rawSecp256k1PubkeyToAddress } from "@cosmjs/launchpad"; +import { Bech32 } from "@cosmjs/encoding"; +import { encodeSecp256k1Signature } from "@cosmjs/launchpad"; import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx"; import { AccountData, DirectSignResponse, OfflineDirectSigner } from "./signer"; @@ -33,7 +35,7 @@ export class DirectSecp256k1Wallet implements OfflineDirectSigner { } private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix); + return Bech32.encode(this.prefix, rawSecp256k1PubkeyToRawAddress(this.pubkey)); } public async getAccounts(): Promise { diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index 08bb9b2f..faa88ac4 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeSecp256k1Pubkey, SinglePubkey as AminoPubKey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; -import { encodeSecp256k1Pubkey, PubKey as LaunchpadPubKey } from "@cosmjs/launchpad"; import { PubKey } from "./codec/cosmos/crypto/secp256k1/keys"; import { Any } from "./codec/google/protobuf/any"; -export function encodePubkey(pubkey: LaunchpadPubKey): Any { +export function encodePubkey(pubkey: AminoPubKey): Any { switch (pubkey.type) { case "tendermint/PubKeySecp256k1": { const pubkeyProto = PubKey.fromPartial({ @@ -21,7 +21,7 @@ export function encodePubkey(pubkey: LaunchpadPubKey): Any { } } -export function decodePubkey(pubkey?: Any | null): LaunchpadPubKey | null { +export function decodePubkey(pubkey?: Any | null): AminoPubKey | null { if (!pubkey || !pubkey.value) { return null; } diff --git a/packages/stargate/package.json b/packages/stargate/package.json index 70925d26..0e68bab6 100644 --- a/packages/stargate/package.json +++ b/packages/stargate/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "@confio/ics23": "^0.6.3", + "@cosmjs/amino": "^0.25.0-alpha.0", "@cosmjs/encoding": "^0.25.0-alpha.0", "@cosmjs/launchpad": "^0.25.0-alpha.0", "@cosmjs/math": "^0.25.0-alpha.0", diff --git a/packages/stargate/src/accounts.ts b/packages/stargate/src/accounts.ts index 58280523..04ab739b 100644 --- a/packages/stargate/src/accounts.ts +++ b/packages/stargate/src/accounts.ts @@ -1,4 +1,4 @@ -import { PubKey } from "@cosmjs/launchpad"; +import { Pubkey } from "@cosmjs/amino"; import { Uint64 } from "@cosmjs/math"; import { decodePubkey } from "@cosmjs/proto-signing"; import { assert } from "@cosmjs/utils"; @@ -16,7 +16,7 @@ import { Any } from "./codec/google/protobuf/any"; export interface Account { /** Bech32 account address */ readonly address: string; - readonly pubkey: PubKey | null; + readonly pubkey: Pubkey | null; readonly accountNumber: number; readonly sequence: number; } diff --git a/packages/stargate/src/aminotypes.spec.ts b/packages/stargate/src/aminotypes.spec.ts index 0c768341..bf598e3c 100644 --- a/packages/stargate/src/aminotypes.spec.ts +++ b/packages/stargate/src/aminotypes.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { encodeBech32Pubkey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { coin, coins, - encodeBech32Pubkey, MsgBeginRedelegate as LaunchpadMsgBeginRedelegate, MsgCreateValidator as LaunchpadMsgCreateValidator, MsgDelegate as LaunchpadMsgDelegate, diff --git a/packages/stargate/src/aminotypes.ts b/packages/stargate/src/aminotypes.ts index b79edf93..7392af22 100644 --- a/packages/stargate/src/aminotypes.ts +++ b/packages/stargate/src/aminotypes.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { decodeBech32Pubkey, encodeBech32Pubkey } from "@cosmjs/amino"; import { fromBase64, toBase64 } from "@cosmjs/encoding"; import { - decodeBech32Pubkey, - encodeBech32Pubkey, Msg, MsgBeginRedelegate as LaunchpadMsgBeginRedelegate, MsgCreateValidator as LaunchpadMsgCreateValidator, diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index b7d5bc75..08d70484 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -1,9 +1,9 @@ +import { encodeSecp256k1Pubkey } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { buildFeeTable, Coin, CosmosFeeTable, - encodeSecp256k1Pubkey, GasLimits, GasPrice, makeSignDoc as makeSignDocAmino, diff --git a/packages/utils/src/array.spec.ts b/packages/utils/src/array.spec.ts index d42241bc..0dae826e 100644 --- a/packages/utils/src/array.spec.ts +++ b/packages/utils/src/array.spec.ts @@ -1,4 +1,4 @@ -import { arrayContentEquals } from "./arrays"; +import { arrayContentEquals, arrayContentStartsWith } from "./arrays"; describe("array", () => { describe("arrayContentEquals", () => { @@ -30,4 +30,32 @@ describe("array", () => { expect(arrayContentEquals([], new Uint8Array([]))).toEqual(true); }); }); + + describe("arrayContentStartsWith", () => { + it("can compare number arrays", () => { + // same length + expect(arrayContentStartsWith([], [])).toEqual(true); // Same behaviour as "".startsWith("") + expect(arrayContentStartsWith([1, 2, 3], [1, 2, 3])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3], [1, 2, 8])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3], [0, 0, 0])).toEqual(false); + + // a shorter than b + expect(arrayContentStartsWith([], [1, 2, 3])).toEqual(false); + expect(arrayContentStartsWith([1], [1, 2, 3])).toEqual(false); + expect(arrayContentStartsWith([1, 2], [1, 2, 3])).toEqual(false); + + // a longer than b + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 4])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [])).toEqual(true); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 4, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 3, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 2, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [1, 0])).toEqual(false); + expect(arrayContentStartsWith([1, 2, 3, 4, 5], [0])).toEqual(false); + }); + }); }); diff --git a/packages/utils/src/arrays.ts b/packages/utils/src/arrays.ts index 87f5a442..4f15b89e 100644 --- a/packages/utils/src/arrays.ts +++ b/packages/utils/src/arrays.ts @@ -16,3 +16,22 @@ export function arrayContentEquals( } return true; } + +/** + * Checks if `a` starts with the contents of `b`. + * + * This requires equality of the 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 arrayContentStartsWith( + a: ArrayLike, + b: ArrayLike, +): boolean { + if (a.length < b.length) return false; + for (let i = 0; i < b.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 372b3615..790c7d27 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,4 @@ -export { arrayContentEquals } from "./arrays"; +export { arrayContentEquals, arrayContentStartsWith } from "./arrays"; export { assert, assertDefined, assertDefinedAndNotNull } from "./assert"; export { sleep } from "./sleep"; export { isNonNullObject, isUint8Array } from "./typechecks";