diff --git a/packages/proto-signing/src/testdata/timestamp.json b/packages/proto-signing/src/testdata/timestamp.json new file mode 100644 index 0000000000..9e4efad085 --- /dev/null +++ b/packages/proto-signing/src/testdata/timestamp.json @@ -0,0 +1,21 @@ +[ + [0, 0, "1970-01-01T00:00:00.000000000Z"], + [0, 1, "1970-01-01T00:00:00.000000001Z"], + [0, 10, "1970-01-01T00:00:00.000000010Z"], + [0, 100, "1970-01-01T00:00:00.000000100Z"], + [0, 1000, "1970-01-01T00:00:00.000001000Z"], + [0, 10000, "1970-01-01T00:00:00.000010000Z"], + [0, 100000, "1970-01-01T00:00:00.000100000Z"], + [0, 1000000, "1970-01-01T00:00:00.001000000Z"], + [0, 10000000, "1970-01-01T00:00:00.010000000Z"], + [0, 100000000, "1970-01-01T00:00:00.100000000Z"], + [0, 999999999, "1970-01-01T00:00:00.999999999Z"], + [0, 1000000000, null], + [1, 0, "1970-01-01T00:00:01.000000000Z"], + [2, 0, "1970-01-01T00:00:02.000000000Z"], + [2, 6, "1970-01-01T00:00:02.000000006Z"], + [2, 6, "1970-01-01T00:00:02.000000006Z"], + [1657797740, 983000000, "2022-07-14T11:22:20.983000000Z"], + [-1, 0, "1969-12-31T23:59:59.000000000Z"], + [0, -1, null] +] diff --git a/packages/proto-signing/src/textual/valuerenderers.spec.ts b/packages/proto-signing/src/textual/valuerenderers.spec.ts index 809b0fc0a7..9a85e8c432 100644 --- a/packages/proto-signing/src/textual/valuerenderers.spec.ts +++ b/packages/proto-signing/src/textual/valuerenderers.spec.ts @@ -6,6 +6,7 @@ import coinData from "../testdata/coin.json"; import coinsData from "../testdata/coins.json"; import decimals from "../testdata/decimals.json"; import integers from "../testdata/integers.json"; +import timestampData from "../testdata/timestamp.json"; import { DisplayUnit, formatBytes, @@ -13,12 +14,15 @@ import { formatCoins, formatDecimal, formatInteger, + formatTimestamp, } from "./valuerenderers"; type TestDataCoin = Array<[Coin, DisplayUnit, string]>; type TestDataCoins = Array<[Coin[], Record, string]>; /** First argument is an array of bytes (0-255), second argument is the expected string */ type TestDataBytes = Array<[number[], string]>; +/** First argument is seconds, second argument is nanor and third argument is the expected string or null on error */ +type TestDataTimestamp = Array<[number, number, string | null]>; describe("valuerenderers", () => { describe("formatInteger", () => { @@ -90,4 +94,25 @@ describe("valuerenderers", () => { } }); }); + + describe("formatTimestamp", () => { + it("works", () => { + expect(formatTimestamp(0, 0)).toEqual("1970-01-01T00:00:00.000000000Z"); + expect(formatTimestamp(0, 1)).toEqual("1970-01-01T00:00:00.000000001Z"); + expect(formatTimestamp(1, 2)).toEqual("1970-01-01T00:00:01.000000002Z"); + expect(formatTimestamp(5, 0)).toEqual("1970-01-01T00:00:05.000000000Z"); + + for (const [seconds, nanos, expected] of timestampData as TestDataTimestamp) { + if (expected === null) { + expect(() => formatTimestamp(seconds, nanos)) + .withContext(`Input seconds=${seconds}, nanos=${nanos}`) + .toThrowError(); + } else { + expect(formatTimestamp(seconds, nanos)) + .withContext(`Input seconds=${seconds}, nanos=${nanos}`) + .toEqual(expected); + } + } + }); + }); }); diff --git a/packages/proto-signing/src/textual/valuerenderers.ts b/packages/proto-signing/src/textual/valuerenderers.ts index 90d06275eb..0fe87ac13e 100644 --- a/packages/proto-signing/src/textual/valuerenderers.ts +++ b/packages/proto-signing/src/textual/valuerenderers.ts @@ -1,5 +1,5 @@ import { toBase64 } from "@cosmjs/encoding"; -import { Decimal } from "@cosmjs/math"; +import { Decimal, Uint32 } from "@cosmjs/math"; import { DenomUnit } from "cosmjs-types/cosmos/bank/v1beta1/bank"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; @@ -40,3 +40,28 @@ export function formatCoins(input: Coin[], units: Record): export function formatBytes(data: Uint8Array): string { return toBase64(data); } + +export interface DateWithNanoseconds extends Date { + /** Nanoseconds after the time stored in a vanilla Date (millisecond granularity) */ + nanoseconds?: number; +} + +function toRfc3339WithNanoseconds(dateTime: DateWithNanoseconds): string { + const millisecondIso = dateTime.toISOString(); + const nanoseconds = dateTime.nanoseconds?.toString() ?? ""; + return `${millisecondIso.slice(0, -1)}${nanoseconds.padStart(6, "0")}Z`; +} + +function fromSeconds(seconds: number, nanos = 0): DateWithNanoseconds { + const checkedNanos = new Uint32(nanos).toNumber(); + if (checkedNanos > 999_999_999) { + throw new Error("Nano seconds must not exceed 999999999"); + } + const out: DateWithNanoseconds = new Date(seconds * 1000 + Math.floor(checkedNanos / 1000000)); + out.nanoseconds = checkedNanos % 1000000; + return out; +} + +export function formatTimestamp(seconds: number, nanos: number): string { + return toRfc3339WithNanoseconds(fromSeconds(seconds, nanos)); +}