Merge pull request #719 from cosmos/support-PubKeyMultisigThreshold

Add address derivation support for MultisigThreshold public keys
This commit is contained in:
mergify[bot] 2021-03-23 13:39:04 +00:00 committed by GitHub
commit 6aa0e3f93a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1114 additions and 359 deletions

View File

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

View File

@ -0,0 +1,8 @@
node_modules/
build/
custom_types/
dist/
docs/
generated/
types/

3
packages/amino/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
dist/
docs/

1
packages/amino/.nycrc.yml Symbolic link
View File

@ -0,0 +1 @@
../../.nycrc.yml

12
packages/amino/README.md Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages

View File

@ -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 <webmaster128@users.noreply.github.com>"
],
"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": {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}

11
packages/amino/typedoc.js Normal file
View File

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

View File

@ -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",
},
},
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*

View File

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

View File

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

View File

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

View File

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

View File

@ -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<readonly AccountData[]> {

View File

@ -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<readonly AccountData[]> {

View File

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

View File

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

View File

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

View File

@ -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<readonly AccountData[]> {

View File

@ -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<readonly AccountData[]> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { encodeSecp256k1Pubkey } from "@cosmjs/amino";
import { fromBase64 } from "@cosmjs/encoding";
import {
buildFeeTable,
Coin,
CosmosFeeTable,
encodeSecp256k1Pubkey,
GasLimits,
GasPrice,
makeSignDoc as makeSignDocAmino,

View File

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

View File

@ -16,3 +16,22 @@ export function arrayContentEquals<T extends string | number | boolean>(
}
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<T extends string | number | boolean>(
a: ArrayLike<T>,
b: ArrayLike<T>,
): 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;
}

View File

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