mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-10 13:47:12 +00:00
Add @cosmjs/encoding and @cosmjs/math
This commit is contained in:
parent
4cca97651b
commit
3cfad7a541
3
NOTICE
3
NOTICE
@ -14,6 +14,9 @@ on 2020-06-05.
|
||||
The code in packages/utils was forked from https://github.com/iov-one/iov-core/tree/v2.3.2/packages/iov-utils
|
||||
on 2020-06-05.
|
||||
|
||||
The code in packages/encoding and packages/math was forked from https://github.com/iov-one/iov-core/tree/v2.3.2/packages/iov-encoding
|
||||
on 2020-06-05.
|
||||
|
||||
Copyright 2018-2020 IOV SAS
|
||||
Copyright 2020 Confio UO
|
||||
Copyright 2020 Simon Warta
|
||||
|
8
packages/encoding/.eslintignore
Normal file
8
packages/encoding/.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
|
||||
build/
|
||||
custom_types/
|
||||
dist/
|
||||
docs/
|
||||
generated/
|
||||
types/
|
3
packages/encoding/.gitignore
vendored
Normal file
3
packages/encoding/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
24
packages/encoding/README.md
Normal file
24
packages/encoding/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# @cosmjs/encoding
|
||||
|
||||
[](https://www.npmjs.com/package/@cosmjs/encoding)
|
||||
|
||||
This package is an extension to the JavaScript standard library that is not
|
||||
bound to blockchain products. It provides basic hex/base64/ascii encoding to
|
||||
Uint8Array that doesn't rely on Buffer and also provides better error messages
|
||||
on invalid input.
|
||||
|
||||
## Convert between bech32 and hex addresses
|
||||
|
||||
```
|
||||
>> Bech32.encode("tiov", fromHex("1234ABCD0000AA0000FFFF0000AA00001234ABCD"))
|
||||
'tiov1zg62hngqqz4qqq8lluqqp2sqqqfrf27dzrrmea'
|
||||
>> toHex(Bech32.decode("tiov1zg62hngqqz4qqq8lluqqp2sqqqfrf27dzrrmea").data)
|
||||
'1234abcd0000aa0000ffff0000aa00001234abcd'
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This package is part of the cosmwasm-js repository, licensed under the Apache
|
||||
License 2.0 (see
|
||||
[NOTICE](https://github.com/confio/cosmwasm-js/blob/master/NOTICE) and
|
||||
[LICENSE](https://github.com/confio/cosmwasm-js/blob/master/LICENSE)).
|
26
packages/encoding/jasmine-testrunner.js
Executable file
26
packages/encoding/jasmine-testrunner.js
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
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 });
|
||||
|
||||
// initialize and execute
|
||||
jasmine.env.clearReporters();
|
||||
jasmine.addReporter(reporter);
|
||||
jasmine.execute();
|
47
packages/encoding/karma.conf.js
Normal file
47
packages/encoding/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/encoding/nonces/README.txt
Normal file
1
packages/encoding/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
47
packages/encoding/package.json
Normal file
47
packages/encoding/package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@cosmjs/encoding",
|
||||
"version": "0.8.0",
|
||||
"description": "Encoding helpers for blockchain projects",
|
||||
"contributors": ["IOV SAS <admin@iov.one>"],
|
||||
"license": "Apache-2.0",
|
||||
"main": "build/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"build/",
|
||||
"types/",
|
||||
"*.md",
|
||||
"!*.spec.*",
|
||||
"!**/testdata/"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CosmWasm/cosmwasm-js/tree/master/packages/encoding"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
|
||||
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
|
||||
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
|
||||
"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",
|
||||
"move-types": "shx rm -r ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
|
||||
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
|
||||
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
|
||||
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
|
||||
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"bech32": "^1.1.4",
|
||||
"readonly-date": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/base64-js": "^1.2.5"
|
||||
}
|
||||
}
|
26
packages/encoding/src/ascii.spec.ts
Normal file
26
packages/encoding/src/ascii.spec.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { fromAscii, toAscii } from "./ascii";
|
||||
|
||||
describe("ascii", () => {
|
||||
it("encodes to ascii", () => {
|
||||
expect(toAscii("")).toEqual(new Uint8Array([]));
|
||||
expect(toAscii("abc")).toEqual(new Uint8Array([0x61, 0x62, 0x63]));
|
||||
expect(toAscii(" ?=-n|~+-*/\\")).toEqual(
|
||||
new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c]),
|
||||
);
|
||||
|
||||
expect(() => toAscii("ö")).toThrow();
|
||||
expect(() => toAscii("ß")).toThrow();
|
||||
});
|
||||
|
||||
it("decodes from ascii", () => {
|
||||
expect(fromAscii(new Uint8Array([]))).toEqual("");
|
||||
expect(fromAscii(new Uint8Array([0x61, 0x62, 0x63]))).toEqual("abc");
|
||||
expect(
|
||||
fromAscii(new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c])),
|
||||
).toEqual(" ?=-n|~+-*/\\");
|
||||
|
||||
expect(() => fromAscii(new Uint8Array([0x00]))).toThrow();
|
||||
expect(() => fromAscii(new Uint8Array([0x7f]))).toThrow();
|
||||
expect(() => fromAscii(new Uint8Array([0xff]))).toThrow();
|
||||
});
|
||||
});
|
31
packages/encoding/src/ascii.ts
Normal file
31
packages/encoding/src/ascii.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export function toAscii(input: string): Uint8Array {
|
||||
const toNums = (str: string): readonly number[] =>
|
||||
str.split("").map((x: string) => {
|
||||
const charCode = x.charCodeAt(0);
|
||||
// 0x00–0x1F control characters
|
||||
// 0x20–0x7E printable characters
|
||||
// 0x7F delete character
|
||||
// 0x80–0xFF out of 7 bit ascii range
|
||||
if (charCode < 0x20 || charCode > 0x7e) {
|
||||
throw new Error("Cannot encode character that is out of printable ASCII range: " + charCode);
|
||||
}
|
||||
return charCode;
|
||||
});
|
||||
return Uint8Array.from(toNums(input));
|
||||
}
|
||||
|
||||
export function fromAscii(data: Uint8Array): string {
|
||||
const fromNums = (listOfNumbers: readonly number[]): readonly string[] =>
|
||||
listOfNumbers.map((x: number): string => {
|
||||
// 0x00–0x1F control characters
|
||||
// 0x20–0x7E printable characters
|
||||
// 0x7F delete character
|
||||
// 0x80–0xFF out of 7 bit ascii range
|
||||
if (x < 0x20 || x > 0x7e) {
|
||||
throw new Error("Cannot decode character that is out of printable ASCII range: " + x);
|
||||
}
|
||||
return String.fromCharCode(x);
|
||||
});
|
||||
|
||||
return fromNums(Array.from(data)).join("");
|
||||
}
|
65
packages/encoding/src/base64.spec.ts
Normal file
65
packages/encoding/src/base64.spec.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { fromBase64, toBase64 } from "./base64";
|
||||
|
||||
describe("base64", () => {
|
||||
it("encodes to base64", () => {
|
||||
expect(toBase64(new Uint8Array([]))).toEqual("");
|
||||
expect(toBase64(new Uint8Array([0x00]))).toEqual("AA==");
|
||||
expect(toBase64(new Uint8Array([0x00, 0x00]))).toEqual("AAA=");
|
||||
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00]))).toEqual("AAAA");
|
||||
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00, 0x00]))).toEqual("AAAAAA==");
|
||||
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00]))).toEqual("AAAAAAA=");
|
||||
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))).toEqual("AAAAAAAA");
|
||||
expect(toBase64(new Uint8Array([0x61]))).toEqual("YQ==");
|
||||
expect(toBase64(new Uint8Array([0x62]))).toEqual("Yg==");
|
||||
expect(toBase64(new Uint8Array([0x63]))).toEqual("Yw==");
|
||||
expect(toBase64(new Uint8Array([0x61, 0x62, 0x63]))).toEqual("YWJj");
|
||||
});
|
||||
|
||||
it("decodes from base64", () => {
|
||||
expect(fromBase64("")).toEqual(new Uint8Array([]));
|
||||
expect(fromBase64("AA==")).toEqual(new Uint8Array([0x00]));
|
||||
expect(fromBase64("AAA=")).toEqual(new Uint8Array([0x00, 0x00]));
|
||||
expect(fromBase64("AAAA")).toEqual(new Uint8Array([0x00, 0x00, 0x00]));
|
||||
expect(fromBase64("AAAAAA==")).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00]));
|
||||
expect(fromBase64("AAAAAAA=")).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
expect(fromBase64("AAAAAAAA")).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
expect(fromBase64("YQ==")).toEqual(new Uint8Array([0x61]));
|
||||
expect(fromBase64("Yg==")).toEqual(new Uint8Array([0x62]));
|
||||
expect(fromBase64("Yw==")).toEqual(new Uint8Array([0x63]));
|
||||
expect(fromBase64("YWJj")).toEqual(new Uint8Array([0x61, 0x62, 0x63]));
|
||||
|
||||
// invalid length
|
||||
expect(() => fromBase64("a")).toThrow();
|
||||
expect(() => fromBase64("aa")).toThrow();
|
||||
expect(() => fromBase64("aaa")).toThrow();
|
||||
|
||||
// proper length including invalid character
|
||||
expect(() => fromBase64("aaa!")).toThrow();
|
||||
expect(() => fromBase64("aaa*")).toThrow();
|
||||
expect(() => fromBase64("aaaä")).toThrow();
|
||||
|
||||
// proper length plus invalid character
|
||||
expect(() => fromBase64("aaaa!")).toThrow();
|
||||
expect(() => fromBase64("aaaa*")).toThrow();
|
||||
expect(() => fromBase64("aaaaä")).toThrow();
|
||||
|
||||
// extra spaces
|
||||
expect(() => fromBase64("aaaa ")).toThrow();
|
||||
expect(() => fromBase64(" aaaa")).toThrow();
|
||||
expect(() => fromBase64("aa aa")).toThrow();
|
||||
expect(() => fromBase64("aaaa\n")).toThrow();
|
||||
expect(() => fromBase64("\naaaa")).toThrow();
|
||||
expect(() => fromBase64("aa\naa")).toThrow();
|
||||
|
||||
// position of =
|
||||
expect(() => fromBase64("=aaa")).toThrow();
|
||||
expect(() => fromBase64("==aa")).toThrow();
|
||||
|
||||
// concatenated base64 strings should not be supported
|
||||
// see https://github.com/beatgammit/base64-js/issues/42
|
||||
expect(() => fromBase64("AAA=AAA=")).toThrow();
|
||||
|
||||
// wrong number of =
|
||||
expect(() => fromBase64("a===")).toThrow();
|
||||
});
|
||||
});
|
12
packages/encoding/src/base64.ts
Normal file
12
packages/encoding/src/base64.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as base64js from "base64-js";
|
||||
|
||||
export function toBase64(data: Uint8Array): string {
|
||||
return base64js.fromByteArray(data);
|
||||
}
|
||||
|
||||
export function fromBase64(base64String: string): Uint8Array {
|
||||
if (!base64String.match(/^[a-zA-Z0-9+/]*={0,2}$/)) {
|
||||
throw new Error("Invalid base64 string format");
|
||||
}
|
||||
return base64js.toByteArray(base64String);
|
||||
}
|
19
packages/encoding/src/bech32.spec.ts
Normal file
19
packages/encoding/src/bech32.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Bech32 } from "./bech32";
|
||||
import { fromHex } from "./hex";
|
||||
|
||||
describe("Bech32", () => {
|
||||
// test data generate using https://github.com/nym-zone/bech32
|
||||
// bech32 -e -h eth 9d4e856e572e442f0a4b2763e72d08a0e99d8ded
|
||||
const ethAddressRaw = fromHex("9d4e856e572e442f0a4b2763e72d08a0e99d8ded");
|
||||
|
||||
it("encodes", () => {
|
||||
expect(Bech32.encode("eth", ethAddressRaw)).toEqual("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw");
|
||||
});
|
||||
|
||||
it("decodes", () => {
|
||||
expect(Bech32.decode("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toEqual({
|
||||
prefix: "eth",
|
||||
data: ethAddressRaw,
|
||||
});
|
||||
});
|
||||
});
|
16
packages/encoding/src/bech32.ts
Normal file
16
packages/encoding/src/bech32.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as bech32 from "bech32";
|
||||
|
||||
export class Bech32 {
|
||||
public static encode(prefix: string, data: Uint8Array): string {
|
||||
const address = bech32.encode(prefix, bech32.toWords(data));
|
||||
return address;
|
||||
}
|
||||
|
||||
public static decode(address: string): { readonly prefix: string; readonly data: Uint8Array } {
|
||||
const decodedAddress = bech32.decode(address);
|
||||
return {
|
||||
prefix: decodedAddress.prefix,
|
||||
data: new Uint8Array(bech32.fromWords(decodedAddress.words)),
|
||||
};
|
||||
}
|
||||
}
|
44
packages/encoding/src/hex.spec.ts
Normal file
44
packages/encoding/src/hex.spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { fromHex, toHex } from "./hex";
|
||||
|
||||
describe("fromHex", () => {
|
||||
it("works", () => {
|
||||
// simple
|
||||
expect(fromHex("")).toEqual(new Uint8Array([]));
|
||||
expect(fromHex("00")).toEqual(new Uint8Array([0x00]));
|
||||
expect(fromHex("01")).toEqual(new Uint8Array([0x01]));
|
||||
expect(fromHex("10")).toEqual(new Uint8Array([0x10]));
|
||||
expect(fromHex("11")).toEqual(new Uint8Array([0x11]));
|
||||
expect(fromHex("112233")).toEqual(new Uint8Array([0x11, 0x22, 0x33]));
|
||||
expect(fromHex("0123456789abcdef")).toEqual(
|
||||
new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
);
|
||||
|
||||
// capital letters
|
||||
expect(fromHex("AA")).toEqual(new Uint8Array([0xaa]));
|
||||
expect(fromHex("aAbBcCdDeEfF")).toEqual(new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]));
|
||||
|
||||
// error
|
||||
expect(() => fromHex("a")).toThrow();
|
||||
expect(() => fromHex("aaa")).toThrow();
|
||||
expect(() => fromHex("a!")).toThrow();
|
||||
expect(() => fromHex("a ")).toThrow();
|
||||
expect(() => fromHex("aa ")).toThrow();
|
||||
expect(() => fromHex(" aa")).toThrow();
|
||||
expect(() => fromHex("a a")).toThrow();
|
||||
expect(() => fromHex("gg")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("toHex", () => {
|
||||
it("works", () => {
|
||||
expect(toHex(new Uint8Array([]))).toEqual("");
|
||||
expect(toHex(new Uint8Array([0x00]))).toEqual("00");
|
||||
expect(toHex(new Uint8Array([0x01]))).toEqual("01");
|
||||
expect(toHex(new Uint8Array([0x10]))).toEqual("10");
|
||||
expect(toHex(new Uint8Array([0x11]))).toEqual("11");
|
||||
expect(toHex(new Uint8Array([0x11, 0x22, 0x33]))).toEqual("112233");
|
||||
expect(toHex(new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]))).toEqual(
|
||||
"0123456789abcdef",
|
||||
);
|
||||
});
|
||||
});
|
23
packages/encoding/src/hex.ts
Normal file
23
packages/encoding/src/hex.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export function toHex(data: Uint8Array): string {
|
||||
let out = "";
|
||||
for (const byte of data) {
|
||||
out += ("0" + byte.toString(16)).slice(-2);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function fromHex(hexstring: string): Uint8Array {
|
||||
if (hexstring.length % 2 !== 0) {
|
||||
throw new Error("hex string length must be a multiple of 2");
|
||||
}
|
||||
|
||||
const listOfInts: number[] = [];
|
||||
for (let i = 0; i < hexstring.length; i += 2) {
|
||||
const hexByteAsString = hexstring.substr(i, 2);
|
||||
if (!hexByteAsString.match(/[0-9a-f]{2}/i)) {
|
||||
throw new Error("hex string contains invalid characters");
|
||||
}
|
||||
listOfInts.push(parseInt(hexByteAsString, 16));
|
||||
}
|
||||
return new Uint8Array(listOfInts);
|
||||
}
|
6
packages/encoding/src/index.ts
Normal file
6
packages/encoding/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { fromAscii, toAscii } from "./ascii";
|
||||
export { fromBase64, toBase64 } from "./base64";
|
||||
export { Bech32 } from "./bech32";
|
||||
export { fromHex, toHex } from "./hex";
|
||||
export { fromRfc3339, toRfc3339 } from "./rfc3339";
|
||||
export { fromUtf8, toUtf8 } from "./utf8";
|
178
packages/encoding/src/rfc3339.spec.ts
Normal file
178
packages/encoding/src/rfc3339.spec.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { fromRfc3339, toRfc3339 } from "./rfc3339";
|
||||
|
||||
describe("RFC3339", () => {
|
||||
it("parses dates with different time zones", () => {
|
||||
// time zone +/- 0
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+00:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-00:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
|
||||
|
||||
// time zone positive (full hours)
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+01:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 1, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+02:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 2, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+03:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 3, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+11:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 11, 12, 13)));
|
||||
|
||||
// time zone negative (full hours)
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-01:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 1, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-02:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 2, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-03:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 3, 12, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-11:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 11, 12, 13)));
|
||||
|
||||
// time zone positive (minutes only)
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+00:01")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 - 1, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+00:30")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 - 30, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+00:45")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 - 45, 13)));
|
||||
|
||||
// time zone negative (minutes only)
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-00:01")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 + 1, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-00:30")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 + 30, 13)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-00:45")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 + 45, 13)));
|
||||
|
||||
// time zone positive (hours and minutes)
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+01:01")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11 - 1, 12 - 1, 13)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+04:30")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11 - 4, 12 - 30, 13)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13+10:20")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11 - 10, 12 - 20, 13)),
|
||||
);
|
||||
|
||||
// time zone negative (hours and minutes)
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-01:01")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11 + 1, 12 + 1, 13)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-04:30")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11 + 4, 12 + 30, 13)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13-10:20")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11 + 10, 12 + 20, 13)),
|
||||
);
|
||||
});
|
||||
|
||||
it("parses dates with milliseconds", () => {
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.123Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.999Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)));
|
||||
});
|
||||
|
||||
it("parses dates with low precision fractional seconds", () => {
|
||||
// 1 digit
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.0Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.1Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 100)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.9Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 900)));
|
||||
|
||||
// 2 digit
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.00Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.12Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 120)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.99Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 990)));
|
||||
});
|
||||
|
||||
it("parses dates with high precision fractional seconds", () => {
|
||||
// everything after the 3rd digit is truncated
|
||||
|
||||
// 4 digits
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.0000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.1234Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.9999Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)));
|
||||
|
||||
// 5 digits
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.00000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.12345Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.99999Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
|
||||
);
|
||||
|
||||
// 6 digits
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.000000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.123456Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.999999Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
|
||||
);
|
||||
|
||||
// 7 digits
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.0000000Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.1234567Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.9999999Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
|
||||
);
|
||||
|
||||
// 8 digits
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.00000000Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.12345678Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.99999999Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
|
||||
);
|
||||
|
||||
// 9 digits
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.000000000Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.123456789Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
|
||||
);
|
||||
expect(fromRfc3339("2002-10-02T11:12:13.999999999Z")).toEqual(
|
||||
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts space separators", () => {
|
||||
// https://tools.ietf.org/html/rfc3339#section-5.6
|
||||
// Applications using this syntax may choose, for the sake of readability,
|
||||
// to specify a full-date and full-time separated by (say) a space character.
|
||||
expect(fromRfc3339("2002-10-02 11:12:13Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
|
||||
});
|
||||
|
||||
it("throws for invalid format", () => {
|
||||
// extra whitespace
|
||||
expect(() => fromRfc3339(" 2002-10-02T11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T11:12:13Z ")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T11:12:13 Z")).toThrow();
|
||||
|
||||
// wrong date separators
|
||||
expect(() => fromRfc3339("2002:10-02T11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10:02T11:12:13Z")).toThrow();
|
||||
|
||||
// wrong time separators
|
||||
expect(() => fromRfc3339("2002-10-02T11-12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T11:12-13Z")).toThrow();
|
||||
|
||||
// wrong separator
|
||||
expect(() => fromRfc3339("2002-10-02TT11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02 T11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T 11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02t11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02x11:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02311:12:13Z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02.11:12:13Z")).toThrow();
|
||||
|
||||
// wrong time zone
|
||||
expect(() => fromRfc3339("2002-10-02T11:12:13")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T11:12:13z")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T11:12:13 00:00")).toThrow();
|
||||
expect(() => fromRfc3339("2002-10-02T11:12:13+0000")).toThrow();
|
||||
|
||||
// wrong fractional seconds
|
||||
expect(() => fromRfc3339("2018-07-30T19:21:12345Z")).toThrow();
|
||||
expect(() => fromRfc3339("2018-07-30T19:21:12.Z")).toThrow();
|
||||
});
|
||||
|
||||
it("encodes dates", () => {
|
||||
expect(toRfc3339(new Date(Date.UTC(0, 0, 1, 0, 0, 0)))).toEqual("1900-01-01T00:00:00.000Z");
|
||||
expect(toRfc3339(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 456)))).toEqual("2002-10-02T11:12:13.456Z");
|
||||
});
|
||||
});
|
58
packages/encoding/src/rfc3339.ts
Normal file
58
packages/encoding/src/rfc3339.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { ReadonlyDate } from "readonly-date";
|
||||
|
||||
const rfc3339Matcher = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?((?:[+-]\d{2}:\d{2})|Z)$/;
|
||||
|
||||
function padded(integer: number, length = 2): string {
|
||||
const filled = "00000" + integer.toString();
|
||||
return filled.substring(filled.length - length);
|
||||
}
|
||||
|
||||
export function fromRfc3339(str: string): ReadonlyDate {
|
||||
const matches = rfc3339Matcher.exec(str);
|
||||
if (!matches) {
|
||||
throw new Error("Date string is not in RFC3339 format");
|
||||
}
|
||||
|
||||
const year = +matches[1];
|
||||
const month = +matches[2];
|
||||
const day = +matches[3];
|
||||
const hour = +matches[4];
|
||||
const minute = +matches[5];
|
||||
const second = +matches[6];
|
||||
|
||||
// fractional seconds match either undefined or a string like ".1", ".123456789"
|
||||
const milliSeconds = matches[7] ? Math.floor(+matches[7] * 1000) : 0;
|
||||
|
||||
let tzOffsetSign: number;
|
||||
let tzOffsetHours: number;
|
||||
let tzOffsetMinutes: number;
|
||||
|
||||
// if timezone is undefined, it must be Z or nothing (otherwise the group would have captured).
|
||||
if (matches[8] === "Z") {
|
||||
tzOffsetSign = 1;
|
||||
tzOffsetHours = 0;
|
||||
tzOffsetMinutes = 0;
|
||||
} else {
|
||||
tzOffsetSign = matches[8].substring(0, 1) === "-" ? -1 : 1;
|
||||
tzOffsetHours = +matches[8].substring(1, 3);
|
||||
tzOffsetMinutes = +matches[8].substring(4, 6);
|
||||
}
|
||||
|
||||
const tzOffset = tzOffsetSign * (tzOffsetHours * 60 + tzOffsetMinutes) * 60; // seconds
|
||||
|
||||
return new ReadonlyDate(
|
||||
ReadonlyDate.UTC(year, month - 1, day, hour, minute, second, milliSeconds) - tzOffset * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
export function toRfc3339(date: Date | ReadonlyDate): string {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = padded(date.getUTCMonth() + 1);
|
||||
const day = padded(date.getUTCDate());
|
||||
const hour = padded(date.getUTCHours());
|
||||
const minute = padded(date.getUTCMinutes());
|
||||
const second = padded(date.getUTCSeconds());
|
||||
const ms = padded(date.getUTCMilliseconds(), 3);
|
||||
|
||||
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}Z`;
|
||||
}
|
62
packages/encoding/src/utf8.spec.ts
Normal file
62
packages/encoding/src/utf8.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { fromUtf8, toUtf8 } from "./utf8";
|
||||
|
||||
describe("utf8", () => {
|
||||
it("encodes ascii strings", () => {
|
||||
expect(toUtf8("")).toEqual(new Uint8Array([]));
|
||||
expect(toUtf8("abc")).toEqual(new Uint8Array([0x61, 0x62, 0x63]));
|
||||
expect(toUtf8(" ?=-n|~+-*/\\")).toEqual(
|
||||
new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c]),
|
||||
);
|
||||
});
|
||||
|
||||
it("decodes ascii string", () => {
|
||||
expect(fromUtf8(new Uint8Array([]))).toEqual("");
|
||||
expect(fromUtf8(new Uint8Array([0x61, 0x62, 0x63]))).toEqual("abc");
|
||||
expect(
|
||||
fromUtf8(new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c])),
|
||||
).toEqual(" ?=-n|~+-*/\\");
|
||||
});
|
||||
|
||||
it("encodes null character", () => {
|
||||
expect(toUtf8("\u0000")).toEqual(new Uint8Array([0x00]));
|
||||
});
|
||||
|
||||
it("decodes null byte", () => {
|
||||
expect(fromUtf8(new Uint8Array([0x00]))).toEqual("\u0000");
|
||||
});
|
||||
|
||||
it("encodes Basic Multilingual Plane strings", () => {
|
||||
expect(toUtf8("ö")).toEqual(new Uint8Array([0xc3, 0xb6]));
|
||||
expect(toUtf8("¥")).toEqual(new Uint8Array([0xc2, 0xa5]));
|
||||
expect(toUtf8("Ф")).toEqual(new Uint8Array([0xd0, 0xa4]));
|
||||
expect(toUtf8("ⱴ")).toEqual(new Uint8Array([0xe2, 0xb1, 0xb4]));
|
||||
expect(toUtf8("ⵘ")).toEqual(new Uint8Array([0xe2, 0xb5, 0x98]));
|
||||
});
|
||||
|
||||
it("decodes Basic Multilingual Plane strings", () => {
|
||||
expect(fromUtf8(new Uint8Array([0xc3, 0xb6]))).toEqual("ö");
|
||||
expect(fromUtf8(new Uint8Array([0xc2, 0xa5]))).toEqual("¥");
|
||||
expect(fromUtf8(new Uint8Array([0xd0, 0xa4]))).toEqual("Ф");
|
||||
expect(fromUtf8(new Uint8Array([0xe2, 0xb1, 0xb4]))).toEqual("ⱴ");
|
||||
expect(fromUtf8(new Uint8Array([0xe2, 0xb5, 0x98]))).toEqual("ⵘ");
|
||||
});
|
||||
|
||||
it("encodes Supplementary Multilingual Plane strings", () => {
|
||||
// U+1F0A1
|
||||
expect(toUtf8("🂡")).toEqual(new Uint8Array([0xf0, 0x9f, 0x82, 0xa1]));
|
||||
// U+1034A
|
||||
expect(toUtf8("𐍊")).toEqual(new Uint8Array([0xf0, 0x90, 0x8d, 0x8a]));
|
||||
});
|
||||
|
||||
it("decodes Supplementary Multilingual Plane strings", () => {
|
||||
// U+1F0A1
|
||||
expect(fromUtf8(new Uint8Array([0xf0, 0x9f, 0x82, 0xa1]))).toEqual("🂡");
|
||||
// U+1034A
|
||||
expect(fromUtf8(new Uint8Array([0xf0, 0x90, 0x8d, 0x8a]))).toEqual("𐍊");
|
||||
});
|
||||
|
||||
it("throws on invalid utf8 bytes", () => {
|
||||
// Broken UTF8 example from https://github.com/nodejs/node/issues/16894
|
||||
expect(() => fromUtf8(new Uint8Array([0xf0, 0x80, 0x80]))).toThrow();
|
||||
});
|
||||
});
|
36
packages/encoding/src/utf8.ts
Normal file
36
packages/encoding/src/utf8.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Global symbols in some environments
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
|
||||
declare const TextEncoder: any | undefined;
|
||||
declare const TextDecoder: any | undefined;
|
||||
|
||||
function isValidUtf8(data: Uint8Array): boolean {
|
||||
const toStringAndBack = Buffer.from(Buffer.from(data).toString("utf8"), "utf8");
|
||||
return Buffer.compare(Buffer.from(data), toStringAndBack) === 0;
|
||||
}
|
||||
|
||||
export function toUtf8(str: string): Uint8Array {
|
||||
// Browser and future nodejs (https://github.com/nodejs/node/issues/20365)
|
||||
if (typeof TextEncoder !== "undefined") {
|
||||
return new TextEncoder().encode(str);
|
||||
}
|
||||
|
||||
// Use Buffer hack instead of nodejs util.TextEncoder to ensure
|
||||
// webpack does not bundle the util module for browsers.
|
||||
return new Uint8Array(Buffer.from(str, "utf8"));
|
||||
}
|
||||
|
||||
export function fromUtf8(data: Uint8Array): string {
|
||||
// Browser and future nodejs (https://github.com/nodejs/node/issues/20365)
|
||||
if (typeof TextDecoder !== "undefined") {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
||||
}
|
||||
|
||||
// Use Buffer hack instead of nodejs util.TextDecoder to ensure
|
||||
// webpack does not bundle the util module for browsers.
|
||||
// Buffer.toString has no fatal option
|
||||
if (!isValidUtf8(data)) {
|
||||
throw new Error("Invalid UTF8 data");
|
||||
}
|
||||
return Buffer.from(data).toString("utf8");
|
||||
}
|
12
packages/encoding/tsconfig.json
Normal file
12
packages/encoding/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
14
packages/encoding/typedoc.js
Normal file
14
packages/encoding/typedoc.js
Normal file
@ -0,0 +1,14 @@
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
src: ["./src"],
|
||||
out: "docs",
|
||||
exclude: "**/*.spec.ts",
|
||||
target: "es6",
|
||||
name: `${packageJson.name} Documentation`,
|
||||
readme: "README.md",
|
||||
mode: "file",
|
||||
excludeExternals: true,
|
||||
excludeNotExported: true,
|
||||
excludePrivate: true,
|
||||
};
|
2
packages/encoding/types/ascii.d.ts
vendored
Normal file
2
packages/encoding/types/ascii.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare function toAscii(input: string): Uint8Array;
|
||||
export declare function fromAscii(data: Uint8Array): string;
|
2
packages/encoding/types/base64.d.ts
vendored
Normal file
2
packages/encoding/types/base64.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare function toBase64(data: Uint8Array): string;
|
||||
export declare function fromBase64(base64String: string): Uint8Array;
|
9
packages/encoding/types/bech32.d.ts
vendored
Normal file
9
packages/encoding/types/bech32.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
export declare class Bech32 {
|
||||
static encode(prefix: string, data: Uint8Array): string;
|
||||
static decode(
|
||||
address: string,
|
||||
): {
|
||||
readonly prefix: string;
|
||||
readonly data: Uint8Array;
|
||||
};
|
||||
}
|
2
packages/encoding/types/hex.d.ts
vendored
Normal file
2
packages/encoding/types/hex.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare function toHex(data: Uint8Array): string;
|
||||
export declare function fromHex(hexstring: string): Uint8Array;
|
6
packages/encoding/types/index.d.ts
vendored
Normal file
6
packages/encoding/types/index.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export { fromAscii, toAscii } from "./ascii";
|
||||
export { fromBase64, toBase64 } from "./base64";
|
||||
export { Bech32 } from "./bech32";
|
||||
export { fromHex, toHex } from "./hex";
|
||||
export { fromRfc3339, toRfc3339 } from "./rfc3339";
|
||||
export { fromUtf8, toUtf8 } from "./utf8";
|
3
packages/encoding/types/rfc3339.d.ts
vendored
Normal file
3
packages/encoding/types/rfc3339.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { ReadonlyDate } from "readonly-date";
|
||||
export declare function fromRfc3339(str: string): ReadonlyDate;
|
||||
export declare function toRfc3339(date: Date | ReadonlyDate): string;
|
2
packages/encoding/types/utf8.d.ts
vendored
Normal file
2
packages/encoding/types/utf8.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare function toUtf8(str: string): Uint8Array;
|
||||
export declare function fromUtf8(data: Uint8Array): string;
|
17
packages/encoding/webpack.web.config.js
Normal file
17
packages/encoding/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",
|
||||
},
|
||||
},
|
||||
];
|
8
packages/math/.eslintignore
Normal file
8
packages/math/.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
|
||||
build/
|
||||
custom_types/
|
||||
dist/
|
||||
docs/
|
||||
generated/
|
||||
types/
|
3
packages/math/.gitignore
vendored
Normal file
3
packages/math/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
10
packages/math/README.md
Normal file
10
packages/math/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# @cosmjs/math
|
||||
|
||||
[](https://www.npmjs.com/package/@cosmjs/math)
|
||||
|
||||
## License
|
||||
|
||||
This package is part of the cosmwasm-js repository, licensed under the Apache
|
||||
License 2.0 (see
|
||||
[NOTICE](https://github.com/confio/cosmwasm-js/blob/master/NOTICE) and
|
||||
[LICENSE](https://github.com/confio/cosmwasm-js/blob/master/LICENSE)).
|
26
packages/math/jasmine-testrunner.js
Executable file
26
packages/math/jasmine-testrunner.js
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
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 });
|
||||
|
||||
// initialize and execute
|
||||
jasmine.env.clearReporters();
|
||||
jasmine.addReporter(reporter);
|
||||
jasmine.execute();
|
47
packages/math/karma.conf.js
Normal file
47
packages/math/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/math/nonces/README.txt
Normal file
1
packages/math/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
49
packages/math/package.json
Normal file
49
packages/math/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@cosmjs/math",
|
||||
"version": "0.8.0",
|
||||
"description": "Math helpers for blockchain projects",
|
||||
"contributors": ["IOV SAS <admin@iov.one>"],
|
||||
"license": "Apache-2.0",
|
||||
"main": "build/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"build/",
|
||||
"types/",
|
||||
"*.md",
|
||||
"!*.spec.*",
|
||||
"!**/testdata/"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CosmWasm/cosmwasm-js/tree/master/packages/math"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
|
||||
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
|
||||
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
|
||||
"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",
|
||||
"move-types": "shx rm -r ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
|
||||
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
|
||||
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
|
||||
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
|
||||
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"bech32": "^1.1.4",
|
||||
"bn.js": "^4.11.8",
|
||||
"readonly-date": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/base64-js": "^1.2.5",
|
||||
"@types/bn.js": "^4.11.6"
|
||||
}
|
||||
}
|
213
packages/math/src/decimal.spec.ts
Normal file
213
packages/math/src/decimal.spec.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { Decimal } from "./decimal";
|
||||
|
||||
describe("Decimal", () => {
|
||||
describe("fromAtomics", () => {
|
||||
it("leads to correct atomics value", () => {
|
||||
expect(Decimal.fromAtomics("1", 0).atomics).toEqual("1");
|
||||
expect(Decimal.fromAtomics("1", 1).atomics).toEqual("1");
|
||||
expect(Decimal.fromAtomics("1", 2).atomics).toEqual("1");
|
||||
|
||||
expect(Decimal.fromAtomics("1", 5).atomics).toEqual("1");
|
||||
expect(Decimal.fromAtomics("2", 5).atomics).toEqual("2");
|
||||
expect(Decimal.fromAtomics("3", 5).atomics).toEqual("3");
|
||||
expect(Decimal.fromAtomics("10", 5).atomics).toEqual("10");
|
||||
expect(Decimal.fromAtomics("20", 5).atomics).toEqual("20");
|
||||
expect(Decimal.fromAtomics("30", 5).atomics).toEqual("30");
|
||||
expect(Decimal.fromAtomics("100000000000000000000000", 5).atomics).toEqual("100000000000000000000000");
|
||||
expect(Decimal.fromAtomics("200000000000000000000000", 5).atomics).toEqual("200000000000000000000000");
|
||||
expect(Decimal.fromAtomics("300000000000000000000000", 5).atomics).toEqual("300000000000000000000000");
|
||||
|
||||
expect(Decimal.fromAtomics("44", 5).atomics).toEqual("44");
|
||||
expect(Decimal.fromAtomics("044", 5).atomics).toEqual("44");
|
||||
expect(Decimal.fromAtomics("0044", 5).atomics).toEqual("44");
|
||||
expect(Decimal.fromAtomics("00044", 5).atomics).toEqual("44");
|
||||
});
|
||||
|
||||
it("reads fractional digits correctly", () => {
|
||||
expect(Decimal.fromAtomics("44", 0).toString()).toEqual("44");
|
||||
expect(Decimal.fromAtomics("44", 1).toString()).toEqual("4.4");
|
||||
expect(Decimal.fromAtomics("44", 2).toString()).toEqual("0.44");
|
||||
expect(Decimal.fromAtomics("44", 3).toString()).toEqual("0.044");
|
||||
expect(Decimal.fromAtomics("44", 4).toString()).toEqual("0.0044");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromUserInput", () => {
|
||||
it("throws helpful error message for invalid characters", () => {
|
||||
expect(() => Decimal.fromUserInput(" 13", 5)).toThrowError(/invalid character at position 1/i);
|
||||
expect(() => Decimal.fromUserInput("1,3", 5)).toThrowError(/invalid character at position 2/i);
|
||||
expect(() => Decimal.fromUserInput("13-", 5)).toThrowError(/invalid character at position 3/i);
|
||||
expect(() => Decimal.fromUserInput("13/", 5)).toThrowError(/invalid character at position 3/i);
|
||||
expect(() => Decimal.fromUserInput("13\\", 5)).toThrowError(/invalid character at position 3/i);
|
||||
});
|
||||
|
||||
it("throws for more than one separator", () => {
|
||||
expect(() => Decimal.fromUserInput("1.3.5", 5)).toThrowError(/more than one separator found/i);
|
||||
expect(() => Decimal.fromUserInput("1..3", 5)).toThrowError(/more than one separator found/i);
|
||||
expect(() => Decimal.fromUserInput("..", 5)).toThrowError(/more than one separator found/i);
|
||||
});
|
||||
|
||||
it("throws for separator only", () => {
|
||||
expect(() => Decimal.fromUserInput(".", 5)).toThrowError(/fractional part missing/i);
|
||||
});
|
||||
|
||||
it("throws for more fractional digits than supported", () => {
|
||||
expect(() => Decimal.fromUserInput("44.123456", 5)).toThrowError(
|
||||
/got more fractional digits than supported/i,
|
||||
);
|
||||
expect(() => Decimal.fromUserInput("44.1", 0)).toThrowError(
|
||||
/got more fractional digits than supported/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws for fractional digits that are not non-negative integers", () => {
|
||||
// no integer
|
||||
expect(() => Decimal.fromUserInput("1", Number.NaN)).toThrowError(
|
||||
/fractional digits is not an integer/i,
|
||||
);
|
||||
expect(() => Decimal.fromUserInput("1", Number.POSITIVE_INFINITY)).toThrowError(
|
||||
/fractional digits is not an integer/i,
|
||||
);
|
||||
expect(() => Decimal.fromUserInput("1", Number.NEGATIVE_INFINITY)).toThrowError(
|
||||
/fractional digits is not an integer/i,
|
||||
);
|
||||
expect(() => Decimal.fromUserInput("1", 1.78945544484)).toThrowError(
|
||||
/fractional digits is not an integer/i,
|
||||
);
|
||||
|
||||
// negative
|
||||
expect(() => Decimal.fromUserInput("1", -1)).toThrowError(/fractional digits must not be negative/i);
|
||||
expect(() => Decimal.fromUserInput("1", Number.MIN_SAFE_INTEGER)).toThrowError(
|
||||
/fractional digits must not be negative/i,
|
||||
);
|
||||
|
||||
// exceeds supported range
|
||||
expect(() => Decimal.fromUserInput("1", 101)).toThrowError(/fractional digits must not exceed 100/i);
|
||||
});
|
||||
|
||||
it("returns correct value", () => {
|
||||
expect(Decimal.fromUserInput("44", 0).atomics).toEqual("44");
|
||||
expect(Decimal.fromUserInput("44", 1).atomics).toEqual("440");
|
||||
expect(Decimal.fromUserInput("44", 2).atomics).toEqual("4400");
|
||||
expect(Decimal.fromUserInput("44", 3).atomics).toEqual("44000");
|
||||
|
||||
expect(Decimal.fromUserInput("44.2", 1).atomics).toEqual("442");
|
||||
expect(Decimal.fromUserInput("44.2", 2).atomics).toEqual("4420");
|
||||
expect(Decimal.fromUserInput("44.2", 3).atomics).toEqual("44200");
|
||||
|
||||
expect(Decimal.fromUserInput("44.1", 6).atomics).toEqual("44100000");
|
||||
expect(Decimal.fromUserInput("44.12", 6).atomics).toEqual("44120000");
|
||||
expect(Decimal.fromUserInput("44.123", 6).atomics).toEqual("44123000");
|
||||
expect(Decimal.fromUserInput("44.1234", 6).atomics).toEqual("44123400");
|
||||
expect(Decimal.fromUserInput("44.12345", 6).atomics).toEqual("44123450");
|
||||
expect(Decimal.fromUserInput("44.123456", 6).atomics).toEqual("44123456");
|
||||
});
|
||||
|
||||
it("cuts leading zeros", () => {
|
||||
expect(Decimal.fromUserInput("4", 2).atomics).toEqual("400");
|
||||
expect(Decimal.fromUserInput("04", 2).atomics).toEqual("400");
|
||||
expect(Decimal.fromUserInput("004", 2).atomics).toEqual("400");
|
||||
});
|
||||
|
||||
it("cuts tailing zeros", () => {
|
||||
expect(Decimal.fromUserInput("4.12", 5).atomics).toEqual("412000");
|
||||
expect(Decimal.fromUserInput("4.120", 5).atomics).toEqual("412000");
|
||||
expect(Decimal.fromUserInput("4.1200", 5).atomics).toEqual("412000");
|
||||
expect(Decimal.fromUserInput("4.12000", 5).atomics).toEqual("412000");
|
||||
expect(Decimal.fromUserInput("4.120000", 5).atomics).toEqual("412000");
|
||||
expect(Decimal.fromUserInput("4.1200000", 5).atomics).toEqual("412000");
|
||||
});
|
||||
|
||||
it("interprets the empty string as zero", () => {
|
||||
expect(Decimal.fromUserInput("", 0).atomics).toEqual("0");
|
||||
expect(Decimal.fromUserInput("", 1).atomics).toEqual("0");
|
||||
expect(Decimal.fromUserInput("", 2).atomics).toEqual("0");
|
||||
expect(Decimal.fromUserInput("", 3).atomics).toEqual("0");
|
||||
});
|
||||
|
||||
it("accepts american notation with skipped leading zero", () => {
|
||||
expect(Decimal.fromUserInput(".1", 3).atomics).toEqual("100");
|
||||
expect(Decimal.fromUserInput(".12", 3).atomics).toEqual("120");
|
||||
expect(Decimal.fromUserInput(".123", 3).atomics).toEqual("123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toString", () => {
|
||||
it("displays no decimal point for full numbers", () => {
|
||||
expect(Decimal.fromUserInput("44", 0).toString()).toEqual("44");
|
||||
expect(Decimal.fromUserInput("44", 1).toString()).toEqual("44");
|
||||
expect(Decimal.fromUserInput("44", 2).toString()).toEqual("44");
|
||||
|
||||
expect(Decimal.fromUserInput("44", 2).toString()).toEqual("44");
|
||||
expect(Decimal.fromUserInput("44.0", 2).toString()).toEqual("44");
|
||||
expect(Decimal.fromUserInput("44.00", 2).toString()).toEqual("44");
|
||||
expect(Decimal.fromUserInput("44.000", 2).toString()).toEqual("44");
|
||||
});
|
||||
|
||||
it("only shows significant digits", () => {
|
||||
expect(Decimal.fromUserInput("44.1", 2).toString()).toEqual("44.1");
|
||||
expect(Decimal.fromUserInput("44.10", 2).toString()).toEqual("44.1");
|
||||
expect(Decimal.fromUserInput("44.100", 2).toString()).toEqual("44.1");
|
||||
});
|
||||
|
||||
it("fills up leading zeros", () => {
|
||||
expect(Decimal.fromAtomics("3", 0).toString()).toEqual("3");
|
||||
expect(Decimal.fromAtomics("3", 1).toString()).toEqual("0.3");
|
||||
expect(Decimal.fromAtomics("3", 2).toString()).toEqual("0.03");
|
||||
expect(Decimal.fromAtomics("3", 3).toString()).toEqual("0.003");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toFloatApproximation", () => {
|
||||
it("works", () => {
|
||||
expect(Decimal.fromUserInput("0", 5).toFloatApproximation()).toEqual(0);
|
||||
expect(Decimal.fromUserInput("1", 5).toFloatApproximation()).toEqual(1);
|
||||
expect(Decimal.fromUserInput("1.5", 5).toFloatApproximation()).toEqual(1.5);
|
||||
expect(Decimal.fromUserInput("0.1", 5).toFloatApproximation()).toEqual(0.1);
|
||||
|
||||
expect(Decimal.fromUserInput("1234500000000000", 5).toFloatApproximation()).toEqual(1.2345e15);
|
||||
expect(Decimal.fromUserInput("1234500000000000.002", 5).toFloatApproximation()).toEqual(1.2345e15);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plus", () => {
|
||||
it("returns correct values", () => {
|
||||
const zero = Decimal.fromUserInput("0", 5);
|
||||
expect(zero.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("0");
|
||||
expect(zero.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("1");
|
||||
expect(zero.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("2");
|
||||
expect(zero.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("2.8");
|
||||
expect(zero.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.12345");
|
||||
|
||||
const one = Decimal.fromUserInput("1", 5);
|
||||
expect(one.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("1");
|
||||
expect(one.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("2");
|
||||
expect(one.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("3");
|
||||
expect(one.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("3.8");
|
||||
expect(one.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("1.12345");
|
||||
|
||||
const oneDotFive = Decimal.fromUserInput("1.5", 5);
|
||||
expect(oneDotFive.plus(Decimal.fromUserInput("0", 5)).toString()).toEqual("1.5");
|
||||
expect(oneDotFive.plus(Decimal.fromUserInput("1", 5)).toString()).toEqual("2.5");
|
||||
expect(oneDotFive.plus(Decimal.fromUserInput("2", 5)).toString()).toEqual("3.5");
|
||||
expect(oneDotFive.plus(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("4.3");
|
||||
expect(oneDotFive.plus(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("1.62345");
|
||||
|
||||
// original value remain unchanged
|
||||
expect(zero.toString()).toEqual("0");
|
||||
expect(one.toString()).toEqual("1");
|
||||
expect(oneDotFive.toString()).toEqual("1.5");
|
||||
});
|
||||
|
||||
it("throws for different fractional digits", () => {
|
||||
const zero = Decimal.fromUserInput("0", 5);
|
||||
expect(() => zero.plus(Decimal.fromUserInput("1", 1))).toThrowError(/do not match/i);
|
||||
expect(() => zero.plus(Decimal.fromUserInput("1", 2))).toThrowError(/do not match/i);
|
||||
expect(() => zero.plus(Decimal.fromUserInput("1", 3))).toThrowError(/do not match/i);
|
||||
expect(() => zero.plus(Decimal.fromUserInput("1", 4))).toThrowError(/do not match/i);
|
||||
|
||||
expect(() => zero.plus(Decimal.fromUserInput("1", 6))).toThrowError(/do not match/i);
|
||||
expect(() => zero.plus(Decimal.fromUserInput("1", 7))).toThrowError(/do not match/i);
|
||||
});
|
||||
});
|
||||
});
|
121
packages/math/src/decimal.ts
Normal file
121
packages/math/src/decimal.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import BN from "bn.js";
|
||||
|
||||
// Too large values lead to massive memory usage. Limit to something sensible.
|
||||
// The largest value we need is 18 (Ether).
|
||||
const maxFractionalDigits = 100;
|
||||
|
||||
/**
|
||||
* A type for arbitrary precision, non-negative decimals.
|
||||
*
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
export class Decimal {
|
||||
public static fromUserInput(input: string, fractionalDigits: number): Decimal {
|
||||
Decimal.verifyFractionalDigits(fractionalDigits);
|
||||
|
||||
const badCharacter = input.match(/[^0-9.]/);
|
||||
if (badCharacter) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
throw new Error(`Invalid character at position ${badCharacter.index! + 1}`);
|
||||
}
|
||||
|
||||
let whole: string;
|
||||
let fractional: string;
|
||||
|
||||
if (input.search(/\./) === -1) {
|
||||
// integer format, no separator
|
||||
whole = input;
|
||||
fractional = "";
|
||||
} else {
|
||||
const parts = input.split(".");
|
||||
switch (parts.length) {
|
||||
case 0:
|
||||
case 1:
|
||||
throw new Error("Fewer than two elements in split result. This must not happen here.");
|
||||
case 2:
|
||||
if (!parts[1]) throw new Error("Fractional part missing");
|
||||
whole = parts[0];
|
||||
fractional = parts[1].replace(/0+$/, "");
|
||||
break;
|
||||
default:
|
||||
throw new Error("More than one separator found");
|
||||
}
|
||||
}
|
||||
|
||||
if (fractional.length > fractionalDigits) {
|
||||
throw new Error("Got more fractional digits than supported");
|
||||
}
|
||||
|
||||
const quantity = `${whole}${fractional.padEnd(fractionalDigits, "0")}`;
|
||||
|
||||
return new Decimal(quantity, fractionalDigits);
|
||||
}
|
||||
|
||||
public static fromAtomics(atomics: string, fractionalDigits: number): Decimal {
|
||||
Decimal.verifyFractionalDigits(fractionalDigits);
|
||||
return new Decimal(atomics, fractionalDigits);
|
||||
}
|
||||
|
||||
private static verifyFractionalDigits(fractionalDigits: number): void {
|
||||
if (!Number.isInteger(fractionalDigits)) throw new Error("Fractional digits is not an integer");
|
||||
if (fractionalDigits < 0) throw new Error("Fractional digits must not be negative");
|
||||
if (fractionalDigits > maxFractionalDigits) {
|
||||
throw new Error(`Fractional digits must not exceed ${maxFractionalDigits}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get atomics(): string {
|
||||
return this.data.atomics.toString();
|
||||
}
|
||||
|
||||
public get fractionalDigits(): number {
|
||||
return this.data.fractionalDigits;
|
||||
}
|
||||
|
||||
private readonly data: {
|
||||
readonly atomics: BN;
|
||||
readonly fractionalDigits: number;
|
||||
};
|
||||
|
||||
private constructor(atomics: string, fractionalDigits: number) {
|
||||
this.data = {
|
||||
atomics: new BN(atomics),
|
||||
fractionalDigits: fractionalDigits,
|
||||
};
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
const factor = new BN(10).pow(new BN(this.data.fractionalDigits));
|
||||
const whole = this.data.atomics.div(factor);
|
||||
const fractional = this.data.atomics.mod(factor);
|
||||
|
||||
if (fractional.isZero()) {
|
||||
return whole.toString();
|
||||
} else {
|
||||
const fullFractionalPart = fractional.toString().padStart(this.data.fractionalDigits, "0");
|
||||
const trimmedFractionalPart = fullFractionalPart.replace(/0+$/, "");
|
||||
return `${whole.toString()}.${trimmedFractionalPart}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an approximation as a float type. Only use this if no
|
||||
* exact calculation is required.
|
||||
*/
|
||||
public toFloatApproximation(): number {
|
||||
const out = Number(this.toString());
|
||||
if (Number.isNaN(out)) throw new Error("Conversion to number failed");
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* a.plus(b) returns a+b.
|
||||
*
|
||||
* Both values need to have the same fractional digits.
|
||||
*/
|
||||
public plus(b: Decimal): Decimal {
|
||||
if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match");
|
||||
const sum = this.data.atomics.add(new BN(b.atomics));
|
||||
return new Decimal(sum.toString(), this.fractionalDigits);
|
||||
}
|
||||
}
|
2
packages/math/src/index.ts
Normal file
2
packages/math/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Decimal } from "./decimal";
|
||||
export { Int53, Uint32, Uint53, Uint64 } from "./integers";
|
475
packages/math/src/integers.spec.ts
Normal file
475
packages/math/src/integers.spec.ts
Normal file
@ -0,0 +1,475 @@
|
||||
import { Int53, Uint32, Uint53, Uint64 } from "./integers";
|
||||
|
||||
describe("Integers", () => {
|
||||
describe("Uint32", () => {
|
||||
it("can be constructed", () => {
|
||||
expect(new Uint32(0)).toBeTruthy();
|
||||
expect(new Uint32(1)).toBeTruthy();
|
||||
expect(new Uint32(1.0)).toBeTruthy();
|
||||
expect(new Uint32(42)).toBeTruthy();
|
||||
expect(new Uint32(1000000000)).toBeTruthy();
|
||||
expect(new Uint32(2147483647)).toBeTruthy();
|
||||
expect(new Uint32(2147483648)).toBeTruthy();
|
||||
expect(new Uint32(4294967295)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("throws for invald numbers", () => {
|
||||
expect(() => new Uint32(NaN)).toThrowError(/not a number/);
|
||||
|
||||
expect(() => new Uint32(1.1)).toThrowError(/not an integer/i);
|
||||
expect(() => new Uint32(Number.NEGATIVE_INFINITY)).toThrowError(/not an integer/i);
|
||||
expect(() => new Uint32(Number.POSITIVE_INFINITY)).toThrowError(/not an integer/i);
|
||||
});
|
||||
|
||||
it("throws for values out of range", () => {
|
||||
expect(() => new Uint32(-1)).toThrowError(/not in uint32 range/);
|
||||
expect(() => new Uint32(4294967296)).toThrowError(/not in uint32 range/);
|
||||
expect(() => new Uint32(Number.MIN_SAFE_INTEGER)).toThrowError(/not in uint32 range/);
|
||||
expect(() => new Uint32(Number.MAX_SAFE_INTEGER)).toThrowError(/not in uint32 range/);
|
||||
});
|
||||
|
||||
it("can convert to number", () => {
|
||||
expect(new Uint32(0).toNumber()).toEqual(0);
|
||||
expect(new Uint32(1).toNumber()).toEqual(1);
|
||||
expect(new Uint32(42).toNumber()).toEqual(42);
|
||||
expect(new Uint32(1000000000).toNumber()).toEqual(1000000000);
|
||||
expect(new Uint32(2147483647).toNumber()).toEqual(2147483647);
|
||||
expect(new Uint32(2147483648).toNumber()).toEqual(2147483648);
|
||||
expect(new Uint32(4294967295).toNumber()).toEqual(4294967295);
|
||||
});
|
||||
|
||||
it("can convert to string", () => {
|
||||
expect(new Uint32(0).toString()).toEqual("0");
|
||||
expect(new Uint32(1).toString()).toEqual("1");
|
||||
expect(new Uint32(42).toString()).toEqual("42");
|
||||
expect(new Uint32(1000000000).toString()).toEqual("1000000000");
|
||||
expect(new Uint32(2147483647).toString()).toEqual("2147483647");
|
||||
expect(new Uint32(2147483648).toString()).toEqual("2147483648");
|
||||
expect(new Uint32(4294967295).toString()).toEqual("4294967295");
|
||||
});
|
||||
|
||||
describe("toBytesBigEndian", () => {
|
||||
it("works", () => {
|
||||
expect(new Uint32(0).toBytesBigEndian()).toEqual(new Uint8Array([0, 0, 0, 0]));
|
||||
expect(new Uint32(1).toBytesBigEndian()).toEqual(new Uint8Array([0, 0, 0, 1]));
|
||||
expect(new Uint32(42).toBytesBigEndian()).toEqual(new Uint8Array([0, 0, 0, 42]));
|
||||
expect(new Uint32(1000000000).toBytesBigEndian()).toEqual(new Uint8Array([0x3b, 0x9a, 0xca, 0x00]));
|
||||
expect(new Uint32(2147483647).toBytesBigEndian()).toEqual(new Uint8Array([0x7f, 0xff, 0xff, 0xff]));
|
||||
expect(new Uint32(2147483648).toBytesBigEndian()).toEqual(new Uint8Array([0x80, 0x00, 0x00, 0x00]));
|
||||
expect(new Uint32(4294967295).toBytesBigEndian()).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("toBytesLittleEndian", () => {
|
||||
it("works", () => {
|
||||
expect(new Uint32(0).toBytesLittleEndian()).toEqual(new Uint8Array([0, 0, 0, 0]));
|
||||
expect(new Uint32(1).toBytesLittleEndian()).toEqual(new Uint8Array([1, 0, 0, 0]));
|
||||
expect(new Uint32(42).toBytesLittleEndian()).toEqual(new Uint8Array([42, 0, 0, 0]));
|
||||
expect(new Uint32(1000000000).toBytesLittleEndian()).toEqual(
|
||||
new Uint8Array([0x00, 0xca, 0x9a, 0x3b]),
|
||||
);
|
||||
expect(new Uint32(2147483647).toBytesLittleEndian()).toEqual(
|
||||
new Uint8Array([0xff, 0xff, 0xff, 0x7f]),
|
||||
);
|
||||
expect(new Uint32(2147483648).toBytesLittleEndian()).toEqual(
|
||||
new Uint8Array([0x00, 0x00, 0x00, 0x80]),
|
||||
);
|
||||
expect(new Uint32(4294967295).toBytesLittleEndian()).toEqual(
|
||||
new Uint8Array([0xff, 0xff, 0xff, 0xff]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromBigEndianBytes", () => {
|
||||
it("can be constructed from to byte array", () => {
|
||||
expect(Uint32.fromBigEndianBytes([0, 0, 0, 0]).toNumber()).toEqual(0);
|
||||
expect(Uint32.fromBigEndianBytes([0, 0, 0, 1]).toNumber()).toEqual(1);
|
||||
expect(Uint32.fromBigEndianBytes([0, 0, 0, 42]).toNumber()).toEqual(42);
|
||||
expect(Uint32.fromBigEndianBytes([0x3b, 0x9a, 0xca, 0x00]).toNumber()).toEqual(1000000000);
|
||||
expect(Uint32.fromBigEndianBytes([0x7f, 0xff, 0xff, 0xff]).toNumber()).toEqual(2147483647);
|
||||
expect(Uint32.fromBigEndianBytes([0x80, 0x00, 0x00, 0x00]).toNumber()).toEqual(2147483648);
|
||||
expect(Uint32.fromBigEndianBytes([0xff, 0xff, 0xff, 0xff]).toNumber()).toEqual(4294967295);
|
||||
});
|
||||
|
||||
it("can be constructed from Buffer", () => {
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0, 0, 0, 0])).toNumber()).toEqual(0);
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0, 0, 0, 1])).toNumber()).toEqual(1);
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0, 0, 0, 42])).toNumber()).toEqual(42);
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0x3b, 0x9a, 0xca, 0x00])).toNumber()).toEqual(
|
||||
1000000000,
|
||||
);
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0x7f, 0xff, 0xff, 0xff])).toNumber()).toEqual(
|
||||
2147483647,
|
||||
);
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0x80, 0x00, 0x00, 0x00])).toNumber()).toEqual(
|
||||
2147483648,
|
||||
);
|
||||
expect(Uint32.fromBigEndianBytes(Buffer.from([0xff, 0xff, 0xff, 0xff])).toNumber()).toEqual(
|
||||
4294967295,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws for invalid input length", () => {
|
||||
expect(() => Uint32.fromBigEndianBytes([])).toThrowError(/Invalid input length/);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0])).toThrowError(/Invalid input length/);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, 0, 0])).toThrowError(/Invalid input length/);
|
||||
});
|
||||
|
||||
it("throws for invalid values", () => {
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, -1])).toThrowError(/Invalid value in byte/);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, 1.5])).toThrowError(/Invalid value in byte/);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, 256])).toThrowError(/Invalid value in byte/);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, NaN])).toThrowError(/Invalid value in byte/);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, Number.NEGATIVE_INFINITY])).toThrowError(
|
||||
/Invalid value in byte/,
|
||||
);
|
||||
expect(() => Uint32.fromBigEndianBytes([0, 0, 0, Number.POSITIVE_INFINITY])).toThrowError(
|
||||
/Invalid value in byte/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Int53", () => {
|
||||
it("can be constructed", () => {
|
||||
expect(new Int53(0)).toBeTruthy();
|
||||
expect(new Int53(1)).toBeTruthy();
|
||||
expect(new Int53(1.0)).toBeTruthy();
|
||||
expect(new Int53(42)).toBeTruthy();
|
||||
expect(new Int53(1000000000)).toBeTruthy();
|
||||
expect(new Int53(2147483647)).toBeTruthy();
|
||||
expect(new Int53(2147483648)).toBeTruthy();
|
||||
expect(new Int53(4294967295)).toBeTruthy();
|
||||
expect(new Int53(9007199254740991)).toBeTruthy();
|
||||
|
||||
expect(new Int53(-1)).toBeTruthy();
|
||||
expect(new Int53(-42)).toBeTruthy();
|
||||
expect(new Int53(-2147483648)).toBeTruthy();
|
||||
expect(new Int53(-2147483649)).toBeTruthy();
|
||||
expect(new Int53(-9007199254740991)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("throws for invald numbers", () => {
|
||||
expect(() => new Int53(NaN)).toThrowError(/not a number/);
|
||||
|
||||
expect(() => new Int53(1.1)).toThrowError(/not an integer/i);
|
||||
expect(() => new Int53(Number.NEGATIVE_INFINITY)).toThrowError(/not an integer/i);
|
||||
expect(() => new Int53(Number.POSITIVE_INFINITY)).toThrowError(/not an integer/i);
|
||||
});
|
||||
|
||||
it("throws for values out of range", () => {
|
||||
expect(() => new Int53(Number.MIN_SAFE_INTEGER - 1)).toThrowError(/not in int53 range/);
|
||||
expect(() => new Int53(Number.MAX_SAFE_INTEGER + 1)).toThrowError(/not in int53 range/);
|
||||
});
|
||||
|
||||
it("can convert to number", () => {
|
||||
expect(new Int53(0).toNumber()).toEqual(0);
|
||||
expect(new Int53(1).toNumber()).toEqual(1);
|
||||
expect(new Int53(42).toNumber()).toEqual(42);
|
||||
expect(new Int53(1000000000).toNumber()).toEqual(1000000000);
|
||||
expect(new Int53(2147483647).toNumber()).toEqual(2147483647);
|
||||
expect(new Int53(2147483648).toNumber()).toEqual(2147483648);
|
||||
expect(new Int53(4294967295).toNumber()).toEqual(4294967295);
|
||||
expect(new Int53(9007199254740991).toNumber()).toEqual(9007199254740991);
|
||||
|
||||
expect(new Int53(-1).toNumber()).toEqual(-1);
|
||||
expect(new Int53(-9007199254740991).toNumber()).toEqual(-9007199254740991);
|
||||
});
|
||||
|
||||
it("can convert to string", () => {
|
||||
expect(new Int53(0).toString()).toEqual("0");
|
||||
expect(new Int53(1).toString()).toEqual("1");
|
||||
expect(new Int53(42).toString()).toEqual("42");
|
||||
expect(new Int53(1000000000).toString()).toEqual("1000000000");
|
||||
expect(new Int53(2147483647).toString()).toEqual("2147483647");
|
||||
expect(new Int53(2147483648).toString()).toEqual("2147483648");
|
||||
expect(new Int53(4294967295).toString()).toEqual("4294967295");
|
||||
expect(new Int53(9007199254740991).toString()).toEqual("9007199254740991");
|
||||
|
||||
expect(new Int53(-1).toString()).toEqual("-1");
|
||||
expect(new Int53(-9007199254740991).toString()).toEqual("-9007199254740991");
|
||||
});
|
||||
|
||||
it("can be constructed from string", () => {
|
||||
expect(Int53.fromString("0").toString()).toEqual("0");
|
||||
expect(Int53.fromString("1").toString()).toEqual("1");
|
||||
expect(Int53.fromString("9007199254740991").toString()).toEqual("9007199254740991");
|
||||
|
||||
expect(Int53.fromString("-1").toString()).toEqual("-1");
|
||||
expect(Int53.fromString("-9007199254740991").toString()).toEqual("-9007199254740991");
|
||||
});
|
||||
|
||||
it("throws for invalid string format", () => {
|
||||
expect(() => Int53.fromString(" 0")).toThrowError(/invalid string format/i);
|
||||
expect(() => Int53.fromString("+0")).toThrowError(/invalid string format/i);
|
||||
expect(() => Int53.fromString("1e6")).toThrowError(/invalid string format/i);
|
||||
|
||||
expect(() => Int53.fromString("9007199254740992")).toThrowError(/input not in int53 range/i);
|
||||
expect(() => Int53.fromString("-9007199254740992")).toThrowError(/input not in int53 range/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Uint53", () => {
|
||||
it("can be constructed", () => {
|
||||
expect(new Uint53(0)).toBeTruthy();
|
||||
expect(new Uint53(1)).toBeTruthy();
|
||||
expect(new Uint53(1.0)).toBeTruthy();
|
||||
expect(new Uint53(42)).toBeTruthy();
|
||||
expect(new Uint53(1000000000)).toBeTruthy();
|
||||
expect(new Uint53(2147483647)).toBeTruthy();
|
||||
expect(new Uint53(2147483648)).toBeTruthy();
|
||||
expect(new Uint53(4294967295)).toBeTruthy();
|
||||
expect(new Uint53(9007199254740991)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("throws for invald numbers", () => {
|
||||
expect(() => new Uint53(NaN)).toThrowError(/not a number/);
|
||||
|
||||
expect(() => new Uint53(1.1)).toThrowError(/not an integer/i);
|
||||
expect(() => new Uint53(Number.NEGATIVE_INFINITY)).toThrowError(/not an integer/i);
|
||||
expect(() => new Uint53(Number.POSITIVE_INFINITY)).toThrowError(/not an integer/i);
|
||||
});
|
||||
|
||||
it("throws for values out of range", () => {
|
||||
expect(() => new Uint53(Number.MIN_SAFE_INTEGER - 1)).toThrowError(/not in int53 range/);
|
||||
expect(() => new Uint53(Number.MAX_SAFE_INTEGER + 1)).toThrowError(/not in int53 range/);
|
||||
});
|
||||
|
||||
it("throws for negative inputs", () => {
|
||||
expect(() => new Uint53(-1)).toThrowError(/is negative/);
|
||||
expect(() => new Uint53(-42)).toThrowError(/is negative/);
|
||||
expect(() => new Uint53(-2147483648)).toThrowError(/is negative/);
|
||||
expect(() => new Uint53(-2147483649)).toThrowError(/is negative/);
|
||||
expect(() => new Uint53(-9007199254740991)).toThrowError(/is negative/);
|
||||
});
|
||||
|
||||
it("can convert to number", () => {
|
||||
expect(new Uint53(0).toNumber()).toEqual(0);
|
||||
expect(new Uint53(1).toNumber()).toEqual(1);
|
||||
expect(new Uint53(42).toNumber()).toEqual(42);
|
||||
expect(new Uint53(1000000000).toNumber()).toEqual(1000000000);
|
||||
expect(new Uint53(2147483647).toNumber()).toEqual(2147483647);
|
||||
expect(new Uint53(2147483648).toNumber()).toEqual(2147483648);
|
||||
expect(new Uint53(4294967295).toNumber()).toEqual(4294967295);
|
||||
expect(new Uint53(9007199254740991).toNumber()).toEqual(9007199254740991);
|
||||
});
|
||||
|
||||
it("can convert to string", () => {
|
||||
expect(new Uint53(0).toString()).toEqual("0");
|
||||
expect(new Uint53(1).toString()).toEqual("1");
|
||||
expect(new Uint53(42).toString()).toEqual("42");
|
||||
expect(new Uint53(1000000000).toString()).toEqual("1000000000");
|
||||
expect(new Uint53(2147483647).toString()).toEqual("2147483647");
|
||||
expect(new Uint53(2147483648).toString()).toEqual("2147483648");
|
||||
expect(new Uint53(4294967295).toString()).toEqual("4294967295");
|
||||
expect(new Uint53(9007199254740991).toString()).toEqual("9007199254740991");
|
||||
});
|
||||
|
||||
it("can be constructed from string", () => {
|
||||
expect(Uint53.fromString("0").toString()).toEqual("0");
|
||||
expect(Uint53.fromString("1").toString()).toEqual("1");
|
||||
expect(Uint53.fromString("9007199254740991").toString()).toEqual("9007199254740991");
|
||||
});
|
||||
|
||||
it("throws for invalid string format", () => {
|
||||
expect(() => Uint53.fromString(" 0")).toThrowError(/invalid string format/i);
|
||||
expect(() => Uint53.fromString("+0")).toThrowError(/invalid string format/i);
|
||||
expect(() => Uint53.fromString("1e6")).toThrowError(/invalid string format/i);
|
||||
|
||||
expect(() => Uint53.fromString("-9007199254740992")).toThrowError(/input not in int53 range/i);
|
||||
expect(() => Uint53.fromString("9007199254740992")).toThrowError(/input not in int53 range/i);
|
||||
|
||||
expect(() => Uint53.fromString("-1")).toThrowError(/input is negative/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Uint64", () => {
|
||||
describe("fromBigEndianBytes", () => {
|
||||
it("can be constructed from bytes", () => {
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
||||
});
|
||||
|
||||
it("can be constructed from Uint8Array", () => {
|
||||
Uint64.fromBytesBigEndian(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
});
|
||||
|
||||
it("throws for wrong number of bytes", () => {
|
||||
expect(() => Uint64.fromBytesBigEndian([])).toThrowError(/invalid input length/i);
|
||||
expect(() => Uint64.fromBytesBigEndian([0x00])).toThrowError(/invalid input length/i);
|
||||
expect(() => Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).toThrowError(
|
||||
/invalid input length/i,
|
||||
);
|
||||
expect(() =>
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
).toThrowError(/invalid input length/i);
|
||||
});
|
||||
|
||||
it("throws for wrong byte value", () => {
|
||||
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, 256])).toThrowError(
|
||||
/invalid value in byte/i,
|
||||
);
|
||||
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, -1])).toThrowError(
|
||||
/invalid value in byte/i,
|
||||
);
|
||||
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, 1.5])).toThrowError(
|
||||
/invalid value in byte/i,
|
||||
);
|
||||
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, Number.NEGATIVE_INFINITY])).toThrowError(
|
||||
/invalid value in byte/i,
|
||||
);
|
||||
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, Number.POSITIVE_INFINITY])).toThrowError(
|
||||
/invalid value in byte/i,
|
||||
);
|
||||
expect(() => Uint64.fromBytesBigEndian([0, 0, 0, 0, 0, 0, 0, Number.NaN])).toThrowError(
|
||||
/invalid value in byte/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromString", () => {
|
||||
it("can be constructed from string", () => {
|
||||
{
|
||||
const a = Uint64.fromString("0");
|
||||
expect(a).toBeTruthy();
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromString("1");
|
||||
expect(a).toBeTruthy();
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromString("01");
|
||||
expect(a).toBeTruthy();
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromString("9999999999999999999");
|
||||
expect(a).toBeTruthy();
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromString("18446744073709551615");
|
||||
expect(a).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for invalid string values", () => {
|
||||
expect(() => Uint64.fromString(" 1")).toThrowError(/invalid string format/i);
|
||||
expect(() => Uint64.fromString("-1")).toThrowError(/invalid string format/i);
|
||||
expect(() => Uint64.fromString("+1")).toThrowError(/invalid string format/i);
|
||||
expect(() => Uint64.fromString("1e6")).toThrowError(/invalid string format/i);
|
||||
});
|
||||
|
||||
it("throws for string values exceeding uint64", () => {
|
||||
expect(() => Uint64.fromString("18446744073709551616")).toThrowError(/input exceeds uint64 range/i);
|
||||
expect(() => Uint64.fromString("99999999999999999999")).toThrowError(/input exceeds uint64 range/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromNumber", () => {
|
||||
it("can be constructed from number", () => {
|
||||
const a = Uint64.fromNumber(0);
|
||||
expect(a.toNumber()).toEqual(0);
|
||||
const b = Uint64.fromNumber(1);
|
||||
expect(b.toNumber()).toEqual(1);
|
||||
const c = Uint64.fromNumber(Number.MAX_SAFE_INTEGER);
|
||||
expect(c.toNumber()).toEqual(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it("throws when constructed from wrong numbers", () => {
|
||||
// not a number
|
||||
expect(() => Uint64.fromNumber(Number.NaN)).toThrowError(/input is not a number/i);
|
||||
|
||||
// not an integer
|
||||
expect(() => Uint64.fromNumber(Number.NEGATIVE_INFINITY)).toThrowError(
|
||||
/input is not a safe integer/i,
|
||||
);
|
||||
expect(() => Uint64.fromNumber(Number.POSITIVE_INFINITY)).toThrowError(
|
||||
/input is not a safe integer/i,
|
||||
);
|
||||
expect(() => Uint64.fromNumber(Number.MAX_SAFE_INTEGER + 1)).toThrowError(
|
||||
/input is not a safe integer/i,
|
||||
);
|
||||
|
||||
// negative integer
|
||||
expect(() => Uint64.fromNumber(-1)).toThrowError(/input is negative/i);
|
||||
expect(() => Uint64.fromNumber(Number.MIN_SAFE_INTEGER)).toThrowError(/input is negative/i);
|
||||
});
|
||||
});
|
||||
|
||||
it("can export bytes (big endian)", () => {
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesBigEndian(),
|
||||
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).toBytesBigEndian(),
|
||||
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesBigEndian(),
|
||||
).toEqual(new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0xab, 0x22, 0xbc, 0x5f, 0xa9, 0x20, 0x4e, 0x0d]).toBytesBigEndian(),
|
||||
).toEqual(new Uint8Array([0xab, 0x22, 0xbc, 0x5f, 0xa9, 0x20, 0x4e, 0x0d]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).toBytesBigEndian(),
|
||||
).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]));
|
||||
});
|
||||
|
||||
it("can export bytes (little endian)", () => {
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesLittleEndian(),
|
||||
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).toBytesLittleEndian(),
|
||||
).toEqual(new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).toBytesLittleEndian(),
|
||||
).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0xab, 0x22, 0xbc, 0x5f, 0xa9, 0x20, 0x4e, 0x0d]).toBytesLittleEndian(),
|
||||
).toEqual(new Uint8Array([0x0d, 0x4e, 0x20, 0xa9, 0x5f, 0xbc, 0x22, 0xab]));
|
||||
expect(
|
||||
Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]).toBytesLittleEndian(),
|
||||
).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]));
|
||||
});
|
||||
|
||||
it("can export strings", () => {
|
||||
{
|
||||
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(a.toString()).toEqual("0");
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
expect(a.toString()).toEqual("1");
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromBytesBigEndian([0x8a, 0xc7, 0x23, 0x04, 0x89, 0xe7, 0xff, 0xff]);
|
||||
expect(a.toString()).toEqual("9999999999999999999");
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
||||
expect(a.toString()).toEqual("18446744073709551615");
|
||||
}
|
||||
});
|
||||
|
||||
it("can export numbers", () => {
|
||||
{
|
||||
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(a.toNumber()).toEqual(0);
|
||||
}
|
||||
{
|
||||
const a = Uint64.fromBytesBigEndian([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
expect(a.toNumber()).toEqual(1);
|
||||
}
|
||||
{
|
||||
// value too large for 53 bit integer
|
||||
const a = Uint64.fromBytesBigEndian([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
||||
expect(() => a.toNumber()).toThrowError(/number can only safely store up to 53 bits/i);
|
||||
}
|
||||
{
|
||||
// Number.MAX_SAFE_INTEGER + 1
|
||||
const a = Uint64.fromBytesBigEndian([0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(() => a.toNumber()).toThrowError(/number can only safely store up to 53 bits/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
212
packages/math/src/integers.ts
Normal file
212
packages/math/src/integers.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import BN from "bn.js";
|
||||
|
||||
const uint64MaxValue = new BN("18446744073709551615", 10, "be");
|
||||
|
||||
/** Internal interface to ensure all integer types can be used equally */
|
||||
interface Integer {
|
||||
readonly toNumber: () => number;
|
||||
readonly toString: () => string;
|
||||
}
|
||||
|
||||
interface WithByteConverters {
|
||||
readonly toBytesBigEndian: () => Uint8Array;
|
||||
readonly toBytesLittleEndian: () => Uint8Array;
|
||||
}
|
||||
|
||||
export class Uint32 implements Integer, WithByteConverters {
|
||||
public static fromBigEndianBytes(bytes: ArrayLike<number>): Uint32 {
|
||||
if (bytes.length !== 4) {
|
||||
throw new Error("Invalid input length. Expected 4 bytes.");
|
||||
}
|
||||
|
||||
for (let i = 0; i < bytes.length; ++i) {
|
||||
if (!Number.isInteger(bytes[i]) || bytes[i] > 255 || bytes[i] < 0) {
|
||||
throw new Error("Invalid value in byte. Found: " + bytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Use mulitiplication instead of shifting since bitwise operators are defined
|
||||
// on SIGNED int32 in JavaScript and we don't want to risk surprises
|
||||
return new Uint32(bytes[0] * 2 ** 24 + bytes[1] * 2 ** 16 + bytes[2] * 2 ** 8 + bytes[3]);
|
||||
}
|
||||
|
||||
protected readonly data: number;
|
||||
|
||||
public constructor(input: number) {
|
||||
if (Number.isNaN(input)) {
|
||||
throw new Error("Input is not a number");
|
||||
}
|
||||
|
||||
if (!Number.isInteger(input)) {
|
||||
throw new Error("Input is not an integer");
|
||||
}
|
||||
|
||||
if (input < 0 || input > 4294967295) {
|
||||
throw new Error("Input not in uint32 range: " + input.toString());
|
||||
}
|
||||
|
||||
this.data = input;
|
||||
}
|
||||
|
||||
public toBytesBigEndian(): Uint8Array {
|
||||
// Use division instead of shifting since bitwise operators are defined
|
||||
// on SIGNED int32 in JavaScript and we don't want to risk surprises
|
||||
return new Uint8Array([
|
||||
Math.floor(this.data / 2 ** 24) & 0xff,
|
||||
Math.floor(this.data / 2 ** 16) & 0xff,
|
||||
Math.floor(this.data / 2 ** 8) & 0xff,
|
||||
Math.floor(this.data / 2 ** 0) & 0xff,
|
||||
]);
|
||||
}
|
||||
|
||||
public toBytesLittleEndian(): Uint8Array {
|
||||
// Use division instead of shifting since bitwise operators are defined
|
||||
// on SIGNED int32 in JavaScript and we don't want to risk surprises
|
||||
return new Uint8Array([
|
||||
Math.floor(this.data / 2 ** 0) & 0xff,
|
||||
Math.floor(this.data / 2 ** 8) & 0xff,
|
||||
Math.floor(this.data / 2 ** 16) & 0xff,
|
||||
Math.floor(this.data / 2 ** 24) & 0xff,
|
||||
]);
|
||||
}
|
||||
|
||||
public toNumber(): number {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.data.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export class Int53 implements Integer {
|
||||
public static fromString(str: string): Int53 {
|
||||
if (!str.match(/^-?[0-9]+$/)) {
|
||||
throw new Error("Invalid string format");
|
||||
}
|
||||
|
||||
return new Int53(Number.parseInt(str, 10));
|
||||
}
|
||||
|
||||
protected readonly data: number;
|
||||
|
||||
public constructor(input: number) {
|
||||
if (Number.isNaN(input)) {
|
||||
throw new Error("Input is not a number");
|
||||
}
|
||||
|
||||
if (!Number.isInteger(input)) {
|
||||
throw new Error("Input is not an integer");
|
||||
}
|
||||
|
||||
if (input < Number.MIN_SAFE_INTEGER || input > Number.MAX_SAFE_INTEGER) {
|
||||
throw new Error("Input not in int53 range: " + input.toString());
|
||||
}
|
||||
|
||||
this.data = input;
|
||||
}
|
||||
|
||||
public toNumber(): number {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.data.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export class Uint53 implements Integer {
|
||||
public static fromString(str: string): Uint53 {
|
||||
const signed = Int53.fromString(str);
|
||||
return new Uint53(signed.toNumber());
|
||||
}
|
||||
|
||||
protected readonly data: Int53;
|
||||
|
||||
public constructor(input: number) {
|
||||
const signed = new Int53(input);
|
||||
if (signed.toNumber() < 0) {
|
||||
throw new Error("Input is negative");
|
||||
}
|
||||
this.data = signed;
|
||||
}
|
||||
|
||||
public toNumber(): number {
|
||||
return this.data.toNumber();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.data.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export class Uint64 implements Integer, WithByteConverters {
|
||||
public static fromBytesBigEndian(bytes: ArrayLike<number>): Uint64 {
|
||||
if (bytes.length !== 8) {
|
||||
throw new Error("Invalid input length. Expected 8 bytes.");
|
||||
}
|
||||
|
||||
for (let i = 0; i < bytes.length; ++i) {
|
||||
if (!Number.isInteger(bytes[i]) || bytes[i] > 255 || bytes[i] < 0) {
|
||||
throw new Error("Invalid value in byte. Found: " + bytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const asArray: number[] = [];
|
||||
for (let i = 0; i < bytes.length; ++i) {
|
||||
asArray.push(bytes[i]);
|
||||
}
|
||||
|
||||
return new Uint64(new BN([...asArray]));
|
||||
}
|
||||
|
||||
public static fromString(str: string): Uint64 {
|
||||
if (!str.match(/^[0-9]+$/)) {
|
||||
throw new Error("Invalid string format");
|
||||
}
|
||||
return new Uint64(new BN(str, 10, "be"));
|
||||
}
|
||||
|
||||
public static fromNumber(input: number): Uint64 {
|
||||
if (Number.isNaN(input)) {
|
||||
throw new Error("Input is not a number");
|
||||
}
|
||||
|
||||
let bigint: BN;
|
||||
try {
|
||||
bigint = new BN(input);
|
||||
} catch {
|
||||
throw new Error("Input is not a safe integer");
|
||||
}
|
||||
return new Uint64(bigint);
|
||||
}
|
||||
|
||||
private readonly data: BN;
|
||||
|
||||
private constructor(data: BN) {
|
||||
if (data.isNeg()) {
|
||||
throw new Error("Input is negative");
|
||||
}
|
||||
if (data.gt(uint64MaxValue)) {
|
||||
throw new Error("Input exceeds uint64 range");
|
||||
}
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public toBytesBigEndian(): Uint8Array {
|
||||
return Uint8Array.from(this.data.toArray("be", 8));
|
||||
}
|
||||
|
||||
public toBytesLittleEndian(): Uint8Array {
|
||||
return Uint8Array.from(this.data.toArray("le", 8));
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.data.toString(10);
|
||||
}
|
||||
|
||||
public toNumber(): number {
|
||||
return this.data.toNumber();
|
||||
}
|
||||
}
|
12
packages/math/tsconfig.json
Normal file
12
packages/math/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
14
packages/math/typedoc.js
Normal file
14
packages/math/typedoc.js
Normal file
@ -0,0 +1,14 @@
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
src: ["./src"],
|
||||
out: "docs",
|
||||
exclude: "**/*.spec.ts",
|
||||
target: "es6",
|
||||
name: `${packageJson.name} Documentation`,
|
||||
readme: "README.md",
|
||||
mode: "file",
|
||||
excludeExternals: true,
|
||||
excludeNotExported: true,
|
||||
excludePrivate: true,
|
||||
};
|
26
packages/math/types/decimal.d.ts
vendored
Normal file
26
packages/math/types/decimal.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* A type for arbitrary precision, non-negative decimals.
|
||||
*
|
||||
* Instances of this class are immutable.
|
||||
*/
|
||||
export declare class Decimal {
|
||||
static fromUserInput(input: string, fractionalDigits: number): Decimal;
|
||||
static fromAtomics(atomics: string, fractionalDigits: number): Decimal;
|
||||
private static verifyFractionalDigits;
|
||||
get atomics(): string;
|
||||
get fractionalDigits(): number;
|
||||
private readonly data;
|
||||
private constructor();
|
||||
toString(): string;
|
||||
/**
|
||||
* Returns an approximation as a float type. Only use this if no
|
||||
* exact calculation is required.
|
||||
*/
|
||||
toFloatApproximation(): number;
|
||||
/**
|
||||
* a.plus(b) returns a+b.
|
||||
*
|
||||
* Both values need to have the same fractional digits.
|
||||
*/
|
||||
plus(b: Decimal): Decimal;
|
||||
}
|
2
packages/math/types/index.d.ts
vendored
Normal file
2
packages/math/types/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export { Decimal } from "./decimal";
|
||||
export { Int53, Uint32, Uint53, Uint64 } from "./integers";
|
44
packages/math/types/integers.d.ts
vendored
Normal file
44
packages/math/types/integers.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
/** Internal interface to ensure all integer types can be used equally */
|
||||
interface Integer {
|
||||
readonly toNumber: () => number;
|
||||
readonly toString: () => string;
|
||||
}
|
||||
interface WithByteConverters {
|
||||
readonly toBytesBigEndian: () => Uint8Array;
|
||||
readonly toBytesLittleEndian: () => Uint8Array;
|
||||
}
|
||||
export declare class Uint32 implements Integer, WithByteConverters {
|
||||
static fromBigEndianBytes(bytes: ArrayLike<number>): Uint32;
|
||||
protected readonly data: number;
|
||||
constructor(input: number);
|
||||
toBytesBigEndian(): Uint8Array;
|
||||
toBytesLittleEndian(): Uint8Array;
|
||||
toNumber(): number;
|
||||
toString(): string;
|
||||
}
|
||||
export declare class Int53 implements Integer {
|
||||
static fromString(str: string): Int53;
|
||||
protected readonly data: number;
|
||||
constructor(input: number);
|
||||
toNumber(): number;
|
||||
toString(): string;
|
||||
}
|
||||
export declare class Uint53 implements Integer {
|
||||
static fromString(str: string): Uint53;
|
||||
protected readonly data: Int53;
|
||||
constructor(input: number);
|
||||
toNumber(): number;
|
||||
toString(): string;
|
||||
}
|
||||
export declare class Uint64 implements Integer, WithByteConverters {
|
||||
static fromBytesBigEndian(bytes: ArrayLike<number>): Uint64;
|
||||
static fromString(str: string): Uint64;
|
||||
static fromNumber(input: number): Uint64;
|
||||
private readonly data;
|
||||
private constructor();
|
||||
toBytesBigEndian(): Uint8Array;
|
||||
toBytesLittleEndian(): Uint8Array;
|
||||
toString(): string;
|
||||
toNumber(): number;
|
||||
}
|
||||
export {};
|
17
packages/math/webpack.web.config.js
Normal file
17
packages/math/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",
|
||||
},
|
||||
},
|
||||
];
|
@ -1,2 +1,3 @@
|
||||
export { assert } from "./assert";
|
||||
export { sleep } from "./sleep";
|
||||
export { isNonNullObject, isUint8Array } from "./typechecks";
|
||||
|
58
packages/utils/src/typechecks.spec.ts
Normal file
58
packages/utils/src/typechecks.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { isNonNullObject, isUint8Array } from "./typechecks";
|
||||
|
||||
describe("typechecks", () => {
|
||||
describe("isNonNullObject", () => {
|
||||
it("returns true for objects", () => {
|
||||
expect(isNonNullObject({})).toEqual(true);
|
||||
expect(isNonNullObject({ foo: 123 })).toEqual(true);
|
||||
expect(isNonNullObject(new Uint8Array([]))).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for arrays", () => {
|
||||
// > object is a type that represents the non-primitive type, i.e.
|
||||
// > anything that is not number, string, boolean, symbol, null, or undefined.
|
||||
// https://www.typescriptlang.org/docs/handbook/basic-types.html#object
|
||||
expect(isNonNullObject([])).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false for null", () => {
|
||||
expect(isNonNullObject(null)).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false for other kind of data", () => {
|
||||
expect(isNonNullObject(undefined)).toEqual(false);
|
||||
expect(isNonNullObject("abc")).toEqual(false);
|
||||
expect(isNonNullObject(123)).toEqual(false);
|
||||
expect(isNonNullObject(true)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isUint8Array", () => {
|
||||
it("returns true for Uint8Arrays", () => {
|
||||
expect(isUint8Array(new Uint8Array())).toEqual(true);
|
||||
expect(isUint8Array(new Uint8Array([1, 2, 3]))).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false for Buffer", () => {
|
||||
// One could start a big debate about whether or not a Buffer is a Uint8Array, which
|
||||
// required a definition of "is a" in a languages that has no proper object oriented
|
||||
// programming support.
|
||||
//
|
||||
// In all our software we use Uint8Array for storing binary data and copy Buffers into
|
||||
// new Uint8Array to make deep equality checks work and to ensure our code works the same
|
||||
// way in browsers and Node.js. So our expectation is: _a Buffer is not an Uint8Array_.
|
||||
expect(isUint8Array(Buffer.from(""))).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false for other kind of data", () => {
|
||||
expect(isUint8Array(undefined)).toEqual(false);
|
||||
expect(isUint8Array("abc")).toEqual(false);
|
||||
expect(isUint8Array(123)).toEqual(false);
|
||||
expect(isUint8Array(true)).toEqual(false);
|
||||
|
||||
expect(isUint8Array([])).toEqual(false);
|
||||
expect(isUint8Array(new Int8Array())).toEqual(false);
|
||||
expect(isUint8Array(new Uint16Array())).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
26
packages/utils/src/typechecks.ts
Normal file
26
packages/utils/src/typechecks.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Checks if data is a non-null object (i.e. matches the TypeScript object type)
|
||||
*/
|
||||
export function isNonNullObject(data: unknown): data is object {
|
||||
return typeof data === "object" && data !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data is an Uint8Array. Note: Buffer is treated as not a Uint8Array
|
||||
*/
|
||||
export function isUint8Array(data: unknown): data is Uint8Array {
|
||||
if (!isNonNullObject(data)) return false;
|
||||
|
||||
// Avoid instanceof check which is unreliable in some JS environments
|
||||
// https://medium.com/@simonwarta/limitations-of-the-instanceof-operator-f4bcdbe7a400
|
||||
|
||||
// Use check that was discussed in https://github.com/crypto-browserify/pbkdf2/pull/81
|
||||
if (Object.prototype.toString.call(data) !== "[object Uint8Array]") return false;
|
||||
|
||||
if (typeof Buffer !== "undefined" && typeof Buffer.isBuffer !== "undefined") {
|
||||
// Buffer.isBuffer is available at runtime
|
||||
if (Buffer.isBuffer(data)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -979,6 +979,11 @@
|
||||
dependencies:
|
||||
"@types/babel-types" "*"
|
||||
|
||||
"@types/base64-js@^1.2.5":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.3.0.tgz#c939fdba49846861caf5a246b165dbf5698a317c"
|
||||
integrity sha512-ZmI0sZGAUNXUfMWboWwi4LcfpoVUYldyN6Oe0oJ5cCsHDU/LlRq8nQKPXhYLOx36QYSW9bNIb1vvRrD6K7Llgw==
|
||||
|
||||
"@types/bn.js@*", "@types/bn.js@^4.11.6":
|
||||
version "4.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
|
||||
|
Loading…
x
Reference in New Issue
Block a user