Merge pull request #719 from cosmos/support-PubKeyMultisigThreshold
Add address derivation support for MultisigThreshold public keys
This commit is contained in:
commit
6aa0e3f93a
12
CHANGELOG.md
12
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
|
||||
|
||||
|
||||
8
packages/amino/.eslintignore
Normal file
8
packages/amino/.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
|
||||
build/
|
||||
custom_types/
|
||||
dist/
|
||||
docs/
|
||||
generated/
|
||||
types/
|
||||
3
packages/amino/.gitignore
vendored
Normal file
3
packages/amino/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
1
packages/amino/.nycrc.yml
Symbolic link
1
packages/amino/.nycrc.yml
Symbolic link
@ -0,0 +1 @@
|
||||
../../.nycrc.yml
|
||||
12
packages/amino/README.md
Normal file
12
packages/amino/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# @cosmjs/amino
|
||||
|
||||
[](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)).
|
||||
39
packages/amino/jasmine-testrunner.js
Executable file
39
packages/amino/jasmine-testrunner.js
Executable 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();
|
||||
47
packages/amino/karma.conf.js
Normal file
47
packages/amino/karma.conf.js
Normal 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,
|
||||
});
|
||||
};
|
||||
1
packages/amino/nonces/README.txt
Normal file
1
packages/amino/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
||||
49
packages/amino/package.json
Normal file
49
packages/amino/package.json
Normal 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": {
|
||||
}
|
||||
}
|
||||
91
packages/amino/src/addresses.spec.ts
Normal file
91
packages/amino/src/addresses.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
38
packages/amino/src/addresses.ts
Normal file
38
packages/amino/src/addresses.ts
Normal 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));
|
||||
}
|
||||
163
packages/amino/src/encoding.spec.ts
Normal file
163
packages/amino/src/encoding.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
127
packages/amino/src/encoding.ts
Normal file
127
packages/amino/src/encoding.ts
Normal 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));
|
||||
}
|
||||
21
packages/amino/src/index.ts
Normal file
21
packages/amino/src/index.ts
Normal 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";
|
||||
84
packages/amino/src/multisig.spec.ts
Normal file
84
packages/amino/src/multisig.spec.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
45
packages/amino/src/multisig.ts
Normal file
45
packages/amino/src/multisig.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
52
packages/amino/src/pubkeys.spec.ts
Normal file
52
packages/amino/src/pubkeys.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
71
packages/amino/src/pubkeys.ts
Normal file
71
packages/amino/src/pubkeys.ts
Normal 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";
|
||||
}
|
||||
73
packages/amino/src/testutils.spec.ts
Normal file
73
packages/amino/src/testutils.spec.ts
Normal 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";
|
||||
11
packages/amino/tsconfig.json
Normal file
11
packages/amino/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
11
packages/amino/typedoc.js
Normal file
11
packages/amino/typedoc.js
Normal 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,
|
||||
};
|
||||
17
packages/amino/webpack.web.config.js
Normal file
17
packages/amino/webpack.web.config.js
Normal 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",
|
||||
},
|
||||
},
|
||||
];
|
||||
30
packages/cli/examples/multisig_address.ts
Normal file
30
packages/cli/examples/multisig_address.ts
Normal 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);
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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",
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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));
|
||||
}
|
||||
@ -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[]> {
|
||||
|
||||
@ -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[]> {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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[]> {
|
||||
|
||||
@ -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[]> {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { encodeSecp256k1Pubkey } from "@cosmjs/amino";
|
||||
import { fromBase64 } from "@cosmjs/encoding";
|
||||
import {
|
||||
buildFeeTable,
|
||||
Coin,
|
||||
CosmosFeeTable,
|
||||
encodeSecp256k1Pubkey,
|
||||
GasLimits,
|
||||
GasPrice,
|
||||
makeSignDoc as makeSignDocAmino,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user