diff --git a/CHANGELOG.md b/CHANGELOG.md index dceef247..68cce41c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,9 +57,17 @@ ### Changed +- @cosmjs/encoding: Change return type of `fromRfc3339` from `ReadonlyDate` to + `Date` as the caller becomes the owner of the object and can safely mutate it + in any way. - @cosmjs/launchpad-ledger: Renamed to @cosmjs/ledger-amino. - @cosmjs/ledger-amino: `LedgerSigner.sign` method renamed `signAmino`. +### Deprecated + +- @cosmjs/tendermint-rpc: Deprecate `DateTime` in favour of the free functions + `fromRfc3339WithNanoseconds` and `toRfc3339WithNanoseconds`. + ## 0.23.2 (2021-01-06) ### Security diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 8b15dc71..d5d267c0 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -35,7 +35,7 @@ import { adaptor34, broadcastTxCommitSuccess, Client as TendermintClient, - DateTime, + toRfc3339WithNanoseconds, } from "@cosmjs/tendermint-rpc"; import { assert } from "@cosmjs/utils"; @@ -114,7 +114,7 @@ export class CosmWasmClient { }, height: response.block.header.height, chainId: response.block.header.chainId, - time: DateTime.encode(response.block.header.time), + time: toRfc3339WithNanoseconds(response.block.header.time), }, txs: response.block.txs, }; diff --git a/packages/encoding/src/rfc3339.ts b/packages/encoding/src/rfc3339.ts index 1b58fe14..180ee995 100644 --- a/packages/encoding/src/rfc3339.ts +++ b/packages/encoding/src/rfc3339.ts @@ -7,7 +7,7 @@ function padded(integer: number, length = 2): string { return filled.substring(filled.length - length); } -export function fromRfc3339(str: string): ReadonlyDate { +export function fromRfc3339(str: string): Date { const matches = rfc3339Matcher.exec(str); if (!matches) { throw new Error("Date string is not in RFC3339 format"); @@ -40,9 +40,8 @@ export function fromRfc3339(str: string): ReadonlyDate { const tzOffset = tzOffsetSign * (tzOffsetHours * 60 + tzOffsetMinutes) * 60; // seconds - return new ReadonlyDate( - ReadonlyDate.UTC(year, month - 1, day, hour, minute, second, milliSeconds) - tzOffset * 1000, - ); + const timestamp = Date.UTC(year, month - 1, day, hour, minute, second, milliSeconds) - tzOffset * 1000; + return new Date(timestamp); } export function toRfc3339(date: Date | ReadonlyDate): string { diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index bc26a5a7..8b6c5b4b 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -15,7 +15,7 @@ import { adaptor34, broadcastTxCommitSuccess, Client as TendermintClient, - DateTime, + toRfc3339WithNanoseconds, } from "@cosmjs/tendermint-rpc"; import { assert, assertDefinedAndNotNull } from "@cosmjs/utils"; import Long from "long"; @@ -185,7 +185,7 @@ export class StargateClient { }, height: response.block.header.height, chainId: response.block.header.chainId, - time: DateTime.encode(response.block.header.time), + time: toRfc3339WithNanoseconds(response.block.header.time), }, txs: response.block.txs, }; diff --git a/packages/tendermint-rpc/src/adaptors/v0-33/hasher.spec.ts b/packages/tendermint-rpc/src/adaptors/v0-33/hasher.spec.ts index cc3081b2..a9f0c4c5 100644 --- a/packages/tendermint-rpc/src/adaptors/v0-33/hasher.spec.ts +++ b/packages/tendermint-rpc/src/adaptors/v0-33/hasher.spec.ts @@ -1,7 +1,7 @@ import { fromBase64, fromHex } from "@cosmjs/encoding"; import { ReadonlyDate } from "readonly-date"; -import { ReadonlyDateWithNanoseconds } from "../../types"; +import { ReadonlyDateWithNanoseconds } from "../../dates"; import { hashBlock, hashTx } from "./hasher"; describe("Hasher", () => { diff --git a/packages/tendermint-rpc/src/adaptors/v0-33/responses.ts b/packages/tendermint-rpc/src/adaptors/v0-33/responses.ts index 02f7355f..b2d6a297 100644 --- a/packages/tendermint-rpc/src/adaptors/v0-33/responses.ts +++ b/packages/tendermint-rpc/src/adaptors/v0-33/responses.ts @@ -3,6 +3,7 @@ import { fromBase64, fromHex } from "@cosmjs/encoding"; import { JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; import { assert } from "@cosmjs/utils"; +import { fromRfc3339WithNanoseconds } from "../../dates"; import { assertArray, assertBoolean, @@ -11,7 +12,6 @@ import { assertObject, assertSet, assertString, - DateTime, dictionaryToStringMap, Integer, may, @@ -328,7 +328,7 @@ function decodeHeader(data: RpcHeader): responses.Header { version: decodeBlockVersion(data.version), chainId: assertNotEmpty(data.chain_id), height: Integer.parse(assertNotEmpty(data.height)), - time: DateTime.decode(assertNotEmpty(data.time)), + time: fromRfc3339WithNanoseconds(assertNotEmpty(data.time)), lastBlockId: decodeBlockId(data.last_block_id), @@ -417,7 +417,7 @@ function decodeCommitSignature(data: RpcSignature): CommitSignature { return { blockIdFlag: decodeBlockIdFlag(data.block_id_flag), validatorAddress: fromHex(data.validator_address), - timestamp: DateTime.decode(assertNotEmpty(data.timestamp)), + timestamp: fromRfc3339WithNanoseconds(assertNotEmpty(data.timestamp)), signature: fromBase64(assertNotEmpty(data.signature)), }; } @@ -488,7 +488,7 @@ interface GenesisResult { function decodeGenesis(data: RpcGenesisResponse): responses.GenesisResponse { return { - genesisTime: DateTime.decode(assertNotEmpty(data.genesis_time)), + genesisTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.genesis_time)), chainId: assertNotEmpty(data.chain_id), consensusParams: decodeConsensusParams(data.consensus_params), validators: data.validators ? assertArray(data.validators).map(decodeValidatorGenesis) : [], @@ -568,7 +568,7 @@ function decodeSyncInfo(data: RpcSyncInfo): responses.SyncInfo { return { latestBlockHash: fromHex(assertNotEmpty(data.latest_block_hash)), latestAppHash: fromHex(assertNotEmpty(data.latest_app_hash)), - latestBlockTime: DateTime.decode(assertNotEmpty(data.latest_block_time)), + latestBlockTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.latest_block_time)), latestBlockHeight: Integer.parse(assertNotEmpty(data.latest_block_height)), catchingUp: assertBoolean(data.catching_up), }; diff --git a/packages/tendermint-rpc/src/dates.spec.ts b/packages/tendermint-rpc/src/dates.spec.ts new file mode 100644 index 00000000..dfdf0b94 --- /dev/null +++ b/packages/tendermint-rpc/src/dates.spec.ts @@ -0,0 +1,90 @@ +import { ReadonlyDate } from "readonly-date"; + +import { DateTime, DateWithNanoseconds, fromRfc3339WithNanoseconds, toRfc3339WithNanoseconds } from "./dates"; + +describe("dates", () => { + describe("fromRfc3339WithNanoseconds", () => { + it("works", () => { + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.778Z").nanoseconds).toEqual(0); + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.7789Z").nanoseconds).toEqual(900000); + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.77809Z").nanoseconds).toEqual(90000); + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.778009Z").nanoseconds).toEqual(9000); + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.7780009Z").nanoseconds).toEqual(900); + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.77800009Z").nanoseconds).toEqual(90); + expect(fromRfc3339WithNanoseconds("2020-12-15T10:57:26.778000009Z").nanoseconds).toEqual(9); + }); + }); + + describe("toRfc3339WithNanoseconds", () => { + it("works", () => { + const date1 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date1 as any).nanoseconds = 0; + expect(toRfc3339WithNanoseconds(date1)).toEqual("2020-12-15T10:57:26.778000000Z"); + const date2 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date2 as any).nanoseconds = 900000; + expect(toRfc3339WithNanoseconds(date2)).toEqual("2020-12-15T10:57:26.778900000Z"); + const date3 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date3 as any).nanoseconds = 90000; + expect(toRfc3339WithNanoseconds(date3)).toEqual("2020-12-15T10:57:26.778090000Z"); + const date4 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date4 as any).nanoseconds = 9000; + expect(toRfc3339WithNanoseconds(date4)).toEqual("2020-12-15T10:57:26.778009000Z"); + const date5 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date5 as any).nanoseconds = 900; + expect(toRfc3339WithNanoseconds(date5)).toEqual("2020-12-15T10:57:26.778000900Z"); + const date6 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date6 as any).nanoseconds = 90; + expect(toRfc3339WithNanoseconds(date6)).toEqual("2020-12-15T10:57:26.778000090Z"); + const date7 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date7 as any).nanoseconds = 9; + expect(toRfc3339WithNanoseconds(date7)).toEqual("2020-12-15T10:57:26.778000009Z"); + }); + + it("works for DateWithNanoseconds", () => { + const date1: DateWithNanoseconds = new Date("2020-12-15T10:57:26.778Z"); + date1.nanoseconds = 1; + expect(toRfc3339WithNanoseconds(date1)).toEqual("2020-12-15T10:57:26.778000001Z"); + }); + + it("works for Date", () => { + const date1 = new Date("2020-12-15T10:57:26.778Z"); + expect(toRfc3339WithNanoseconds(date1)).toEqual("2020-12-15T10:57:26.778000000Z"); + }); + }); + + describe("DateTime", () => { + it("decodes a string", () => { + expect(DateTime.decode("2020-12-15T10:57:26.778Z").nanoseconds).toEqual(0); + expect(DateTime.decode("2020-12-15T10:57:26.7789Z").nanoseconds).toEqual(900000); + expect(DateTime.decode("2020-12-15T10:57:26.77809Z").nanoseconds).toEqual(90000); + expect(DateTime.decode("2020-12-15T10:57:26.778009Z").nanoseconds).toEqual(9000); + expect(DateTime.decode("2020-12-15T10:57:26.7780009Z").nanoseconds).toEqual(900); + expect(DateTime.decode("2020-12-15T10:57:26.77800009Z").nanoseconds).toEqual(90); + expect(DateTime.decode("2020-12-15T10:57:26.778000009Z").nanoseconds).toEqual(9); + }); + + it("encodes a string", () => { + const date1 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date1 as any).nanoseconds = 0; + expect(DateTime.encode(date1)).toEqual("2020-12-15T10:57:26.778000000Z"); + const date2 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date2 as any).nanoseconds = 900000; + expect(DateTime.encode(date2)).toEqual("2020-12-15T10:57:26.778900000Z"); + const date3 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date3 as any).nanoseconds = 90000; + expect(DateTime.encode(date3)).toEqual("2020-12-15T10:57:26.778090000Z"); + const date4 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date4 as any).nanoseconds = 9000; + expect(DateTime.encode(date4)).toEqual("2020-12-15T10:57:26.778009000Z"); + const date5 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date5 as any).nanoseconds = 900; + expect(DateTime.encode(date5)).toEqual("2020-12-15T10:57:26.778000900Z"); + const date6 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date6 as any).nanoseconds = 90; + expect(DateTime.encode(date6)).toEqual("2020-12-15T10:57:26.778000090Z"); + const date7 = new ReadonlyDate("2020-12-15T10:57:26.778Z"); + (date7 as any).nanoseconds = 9; + expect(DateTime.encode(date7)).toEqual("2020-12-15T10:57:26.778000009Z"); + }); + }); +}); diff --git a/packages/tendermint-rpc/src/dates.ts b/packages/tendermint-rpc/src/dates.ts new file mode 100644 index 00000000..4a15ccfc --- /dev/null +++ b/packages/tendermint-rpc/src/dates.ts @@ -0,0 +1,39 @@ +import { fromRfc3339 } from "@cosmjs/encoding"; +import { ReadonlyDate } from "readonly-date"; + +export interface ReadonlyDateWithNanoseconds extends ReadonlyDate { + /* Nanoseconds after the time stored in a vanilla ReadonlyDate (millisecond granularity) */ + readonly nanoseconds?: number; +} + +export interface DateWithNanoseconds extends Date { + /** Nanoseconds after the time stored in a vanilla Date (millisecond granularity) */ + nanoseconds?: number; +} + +export function fromRfc3339WithNanoseconds(dateTimeString: string): DateWithNanoseconds { + const out: DateWithNanoseconds = fromRfc3339(dateTimeString); + const nanosecondsMatch = dateTimeString.match(/\.(\d+)Z$/); + const nanoseconds = nanosecondsMatch ? nanosecondsMatch[1].slice(3) : ""; + out.nanoseconds = parseInt(nanoseconds.padEnd(6, "0"), 10); + return out; +} + +export function toRfc3339WithNanoseconds(dateTime: ReadonlyDateWithNanoseconds): string { + const millisecondIso = dateTime.toISOString(); + const nanoseconds = dateTime.nanoseconds?.toString() ?? ""; + return `${millisecondIso.slice(0, -1)}${nanoseconds.padStart(6, "0")}Z`; +} + +/** @deprecated Use fromRfc3339WithNanoseconds/toRfc3339WithNanoseconds instead */ +export class DateTime { + /** @deprecated Use fromRfc3339WithNanoseconds instead */ + public static decode(dateTimeString: string): ReadonlyDateWithNanoseconds { + return fromRfc3339WithNanoseconds(dateTimeString); + } + + /** @deprecated Use toRfc3339WithNanoseconds instead */ + public static encode(dateTime: ReadonlyDateWithNanoseconds): string { + return toRfc3339WithNanoseconds(dateTime); + } +} diff --git a/packages/tendermint-rpc/src/encodings.spec.ts b/packages/tendermint-rpc/src/encodings.spec.ts index 88750e68..afea0600 100644 --- a/packages/tendermint-rpc/src/encodings.spec.ts +++ b/packages/tendermint-rpc/src/encodings.spec.ts @@ -1,53 +1,8 @@ import { ReadonlyDate } from "readonly-date"; -import { - DateTime, - encodeBlockId, - encodeBytes, - encodeInt, - encodeString, - encodeTime, - encodeVersion, -} from "./encodings"; -import { ReadonlyDateWithNanoseconds } from "./types"; +import { encodeBlockId, encodeBytes, encodeInt, encodeString, encodeTime, encodeVersion } from "./encodings"; describe("encodings", () => { - describe("DateTime", () => { - it("decodes a string", () => { - expect(DateTime.decode("2020-12-15T10:57:26.778Z").nanoseconds).toEqual(0); - expect(DateTime.decode("2020-12-15T10:57:26.7789Z").nanoseconds).toEqual(900000); - expect(DateTime.decode("2020-12-15T10:57:26.77809Z").nanoseconds).toEqual(90000); - expect(DateTime.decode("2020-12-15T10:57:26.778009Z").nanoseconds).toEqual(9000); - expect(DateTime.decode("2020-12-15T10:57:26.7780009Z").nanoseconds).toEqual(900); - expect(DateTime.decode("2020-12-15T10:57:26.77800009Z").nanoseconds).toEqual(90); - expect(DateTime.decode("2020-12-15T10:57:26.778000009Z").nanoseconds).toEqual(9); - }); - - it("encodes a string", () => { - const date1 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date1 as any).nanoseconds = 0; - expect(DateTime.encode(date1)).toEqual("2020-12-15T10:57:26.778000000Z"); - const date2 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date2 as any).nanoseconds = 900000; - expect(DateTime.encode(date2)).toEqual("2020-12-15T10:57:26.778900000Z"); - const date3 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date3 as any).nanoseconds = 90000; - expect(DateTime.encode(date3)).toEqual("2020-12-15T10:57:26.778090000Z"); - const date4 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date4 as any).nanoseconds = 9000; - expect(DateTime.encode(date4)).toEqual("2020-12-15T10:57:26.778009000Z"); - const date5 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date5 as any).nanoseconds = 900; - expect(DateTime.encode(date5)).toEqual("2020-12-15T10:57:26.778000900Z"); - const date6 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date6 as any).nanoseconds = 90; - expect(DateTime.encode(date6)).toEqual("2020-12-15T10:57:26.778000090Z"); - const date7 = new ReadonlyDate("2020-12-15T10:57:26.778Z") as ReadonlyDateWithNanoseconds; - (date7 as any).nanoseconds = 9; - expect(DateTime.encode(date7)).toEqual("2020-12-15T10:57:26.778000009Z"); - }); - }); - describe("encodeString", () => { it("works", () => { expect(encodeString("")).toEqual(Uint8Array.from([0])); diff --git a/packages/tendermint-rpc/src/encodings.ts b/packages/tendermint-rpc/src/encodings.ts index 59f6e042..ef7727ee 100644 --- a/packages/tendermint-rpc/src/encodings.ts +++ b/packages/tendermint-rpc/src/encodings.ts @@ -1,8 +1,8 @@ -import { fromRfc3339, toUtf8 } from "@cosmjs/encoding"; +import { toUtf8 } from "@cosmjs/encoding"; import { Int53 } from "@cosmjs/math"; +import { ReadonlyDateWithNanoseconds } from "./dates"; import { BlockId, Version } from "./responses"; -import { ReadonlyDateWithNanoseconds } from "./types"; /** * A runtime checker that ensures a given value is set (i.e. not undefined or null) @@ -156,22 +156,6 @@ export class Integer { } } -export class DateTime { - public static decode(dateTimeString: string): ReadonlyDateWithNanoseconds { - const readonlyDate = fromRfc3339(dateTimeString); - const nanosecondsMatch = dateTimeString.match(/\.(\d+)Z$/); - const nanoseconds = nanosecondsMatch ? nanosecondsMatch[1].slice(3) : ""; - (readonlyDate as any).nanoseconds = parseInt(nanoseconds.padEnd(6, "0"), 10); - return readonlyDate as ReadonlyDateWithNanoseconds; - } - - public static encode(dateTime: ReadonlyDateWithNanoseconds): string { - const millisecondIso = dateTime.toISOString(); - const nanoseconds = dateTime.nanoseconds?.toString() ?? ""; - return `${millisecondIso.slice(0, -1)}${nanoseconds.padStart(6, "0")}Z`; - } -} - // Encodings needed for hashing block headers // Several of these functions are inspired by https://github.com/nomic-io/js-tendermint/blob/tendermint-0.30/src/ diff --git a/packages/tendermint-rpc/src/index.ts b/packages/tendermint-rpc/src/index.ts index df736fb1..c6584a41 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -1,7 +1,12 @@ export { Adaptor } from "./adaptor"; export { adaptor33, adaptor34 } from "./adaptors"; export { Client } from "./client"; -export { DateTime } from "./encodings"; +export { + DateTime, + ReadonlyDateWithNanoseconds, + fromRfc3339WithNanoseconds, + toRfc3339WithNanoseconds, +} from "./dates"; export { AbciInfoRequest, AbciQueryParams, @@ -72,10 +77,4 @@ export { VoteType, } from "./responses"; export { HttpClient, WebsocketClient } from "./rpcclients"; // TODO: Why do we export those outside of this package? -export { - BlockIdFlag, - CommitSignature, - ReadonlyDateWithNanoseconds, - ValidatorEd25519Pubkey, - ValidatorPubkey, -} from "./types"; +export { BlockIdFlag, CommitSignature, ValidatorEd25519Pubkey, ValidatorPubkey } from "./types"; diff --git a/packages/tendermint-rpc/src/responses.ts b/packages/tendermint-rpc/src/responses.ts index 83e7ab65..5706726a 100644 --- a/packages/tendermint-rpc/src/responses.ts +++ b/packages/tendermint-rpc/src/responses.ts @@ -1,6 +1,7 @@ import { ReadonlyDate } from "readonly-date"; -import { CommitSignature, ReadonlyDateWithNanoseconds, ValidatorPubkey } from "./types"; +import { ReadonlyDateWithNanoseconds } from "./dates"; +import { CommitSignature, ValidatorPubkey } from "./types"; export type Response = | AbciInfoResponse diff --git a/packages/tendermint-rpc/src/types.ts b/packages/tendermint-rpc/src/types.ts index c71a10f7..1c62851c 100644 --- a/packages/tendermint-rpc/src/types.ts +++ b/packages/tendermint-rpc/src/types.ts @@ -1,11 +1,7 @@ // Types in this file are exported outside of the @cosmjs/tendermint-rpc package, // e.g. as part of a request or response -import { ReadonlyDate } from "readonly-date"; -export interface ReadonlyDateWithNanoseconds extends ReadonlyDate { - /* Nanoseconds after the time stored in a vanilla ReadonlyDate (millisecond granularity) */ - readonly nanoseconds?: number; -} +import { ReadonlyDateWithNanoseconds } from "./dates"; export interface ValidatorEd25519Pubkey { readonly algorithm: "ed25519";