From ee8f1894fbdb077bda4cb140a427bc32806b0b34 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 10 Dec 2025 11:22:52 +0100 Subject: [PATCH 1/6] fix: set time using bigints --- bun.lock | 3 ++ packages/pic/package.json | 3 +- packages/pic/src/http2-client.ts | 8 ++++-- packages/pic/src/pocket-ic-client-types.ts | 32 +++++++++++++++------- packages/pic/src/pocket-ic.ts | 20 ++++++++------ packages/pic/tests/src/time.spec.ts | 25 +++++++++++++++++ pnpm-lock.yaml | 8 ++++++ 7 files changed, 77 insertions(+), 22 deletions(-) diff --git a/bun.lock b/bun.lock index 53f8cc3..1021774 100644 --- a/bun.lock +++ b/bun.lock @@ -82,6 +82,7 @@ "@dfinity/identity": "^3.2.4", "@dfinity/principal": "^3.2.4", "bip39": "^3.1.0", + "json-with-bigint": "^3.4.4", }, }, "packages/pic-server": { @@ -787,6 +788,8 @@ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-with-bigint": ["json-with-bigint@3.4.4", "", {}, "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], diff --git a/packages/pic/package.json b/packages/pic/package.json index b4a6916..0fe8b95 100644 --- a/packages/pic/package.json +++ b/packages/pic/package.json @@ -33,6 +33,7 @@ "@dfinity/candid": "^3.2.4", "@dfinity/identity": "^3.2.4", "@dfinity/principal": "^3.2.4", - "bip39": "^3.1.0" + "bip39": "^3.1.0", + "json-with-bigint": "^3.4.4" } } diff --git a/packages/pic/src/http2-client.ts b/packages/pic/src/http2-client.ts index 7ff5085..0188251 100644 --- a/packages/pic/src/http2-client.ts +++ b/packages/pic/src/http2-client.ts @@ -1,3 +1,4 @@ +import { JSONParse, JSONStringify } from 'json-with-bigint'; import { ServerRequestTimeoutError } from './error'; import { isNil, poll } from './util'; @@ -121,7 +122,7 @@ export class Http2Client { public async jsonPost(init: JsonPostRequest): Promise { const reqBody = init.body - ? new TextEncoder().encode(JSON.stringify(init.body)) + ? new TextEncoder().encode(JSONStringify(init.body)) : undefined; // poll the request until it is successful or times out @@ -201,10 +202,11 @@ export class Http2Client { async function getResBody( res: Response, ): Promise> { + const resBody = await res.text(); try { - return (await res.clone().json()) as ApiResponse; + return JSONParse(resBody) as ApiResponse; } catch (error) { - const message = await res.text(); + const message = resBody; console.error('Error parsing PocketIC server response body:', error); console.error('Original body:', message); diff --git a/packages/pic/src/pocket-ic-client-types.ts b/packages/pic/src/pocket-ic-client-types.ts index fca8813..d43dbe6 100644 --- a/packages/pic/src/pocket-ic-client-types.ts +++ b/packages/pic/src/pocket-ic-client-types.ts @@ -10,6 +10,8 @@ import { } from './util'; import { TopologyValidationError } from './error'; +const NANOS_PER_MILLISECOND = BigInt(1_000_000); + //#region CreateInstance export interface CreateInstanceRequest { @@ -494,19 +496,19 @@ export function decodeGetControllersResponse( //#region GetTime -export interface GetTimeResponse { - millisSinceEpoch: number; -} +export type GetTimeResponse = { + nanosSinceEpoch: bigint; +}; export interface EncodedGetTimeResponse { - nanos_since_epoch: number; + nanos_since_epoch: bigint; } export function decodeGetTimeResponse( res: EncodedGetTimeResponse, ): GetTimeResponse { return { - millisSinceEpoch: res.nanos_since_epoch / 1_000_000, + nanosSinceEpoch: res.nanos_since_epoch, }; } @@ -514,19 +516,29 @@ export function decodeGetTimeResponse( //#region SetTime -export interface SetTimeRequest { - millisSinceEpoch: number; -} +export type SetTimeRequest = + | { + millisSinceEpoch: number; + } + | { + nanosSinceEpoch: bigint; + }; export interface EncodedSetTimeRequest { - nanos_since_epoch: number; + nanos_since_epoch: bigint; } export function encodeSetTimeRequest( req: SetTimeRequest, ): EncodedSetTimeRequest { + if ('millisSinceEpoch' in req) { + return { + nanos_since_epoch: BigInt(req.millisSinceEpoch) * NANOS_PER_MILLISECOND, + }; + } + return { - nanos_since_epoch: req.millisSinceEpoch * 1_000_000, + nanos_since_epoch: req.nanosSinceEpoch, }; } diff --git a/packages/pic/src/pocket-ic.ts b/packages/pic/src/pocket-ic.ts index 3d8c970..0b04321 100644 --- a/packages/pic/src/pocket-ic.ts +++ b/packages/pic/src/pocket-ic.ts @@ -34,6 +34,8 @@ import { DeferredActor, } from './pocket-ic-deferred-actor'; +const NANOS_PER_MILLISECOND = BigInt(1_000_000); + /** * This class represents the main PocketIC client. * It is responsible for interacting with the PocketIC server via the REST API. @@ -837,9 +839,9 @@ export class PocketIc { * ``` */ public async getTime(): Promise { - const { millisSinceEpoch } = await this.client.getTime(); + const { nanosSinceEpoch } = await this.client.getTime(); - return millisSinceEpoch; + return Number(nanosSinceEpoch / NANOS_PER_MILLISECOND); } /** @@ -1001,9 +1003,10 @@ export class PocketIc { * ``` */ public async advanceTime(duration: number): Promise { - const currentTime = await this.getTime(); - const newTime = currentTime + duration; - await this.setTime(newTime); + const { nanosSinceEpoch } = await this.client.getTime(); + const durationNanos = BigInt(duration) * NANOS_PER_MILLISECOND; + const newTimeNanos = nanosSinceEpoch + durationNanos; + await this.client.setTime({ nanosSinceEpoch: newTimeNanos }); } /** @@ -1031,9 +1034,10 @@ export class PocketIc { * ``` */ public async advanceCertifiedTime(duration: number): Promise { - const currentTime = await this.getTime(); - const newTime = currentTime + duration; - await this.setCertifiedTime(newTime); + const { nanosSinceEpoch } = await this.client.getTime(); + const durationNanos = BigInt(duration) * NANOS_PER_MILLISECOND; + const newTimeNanos = nanosSinceEpoch + durationNanos; + await this.client.setCertifiedTime({ nanosSinceEpoch: newTimeNanos }); } /** diff --git a/packages/pic/tests/src/time.spec.ts b/packages/pic/tests/src/time.spec.ts index e497607..6d4f88f 100644 --- a/packages/pic/tests/src/time.spec.ts +++ b/packages/pic/tests/src/time.spec.ts @@ -119,4 +119,29 @@ describe('time', () => { jest.useRealTimers(); }); + + it('should advance time by 0 without setting time into the past', async () => { + const initialTime = await fixture.actor.get_time(); + + // This should not throw "SettingTimeIntoPast" error + await fixture.pic.advanceTime(0); + await fixture.pic.tick(); + + const finalTime = await fixture.actor.get_time(); + + // Time should not go backwards + expect(finalTime).toBeGreaterThanOrEqual(initialTime); + }); + + it('should advance certified time by 0 without setting time into the past', async () => { + const initialTime = await fixture.actor.get_time(); + + // This should not throw "SettingTimeIntoPast" error + await fixture.pic.advanceCertifiedTime(0); + + const finalTime = await fixture.actor.get_time(); + + // Time should not go backwards + expect(finalTime).toEqual(initialTime); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ed094d..2767c7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: bip39: specifier: ^3.1.0 version: 3.1.0 + json-with-bigint: + specifier: ^3.4.4 + version: 3.4.4 packages/pic-server: {} @@ -1655,6 +1658,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-with-bigint@3.4.4: + resolution: {integrity: sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4240,6 +4246,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-with-bigint@3.4.4: {} + json5@2.2.3: {} kleur@3.0.3: {} From 09bf0ebc857fa4fe78d0ac86f58f67e0a645e9fd Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 10 Dec 2025 11:27:57 +0100 Subject: [PATCH 2/6] build: pin package version --- bun.lock | 2 +- packages/pic/package.json | 2 +- pnpm-lock.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 1021774..655e395 100644 --- a/bun.lock +++ b/bun.lock @@ -82,7 +82,7 @@ "@dfinity/identity": "^3.2.4", "@dfinity/principal": "^3.2.4", "bip39": "^3.1.0", - "json-with-bigint": "^3.4.4", + "json-with-bigint": "3.4.4", }, }, "packages/pic-server": { diff --git a/packages/pic/package.json b/packages/pic/package.json index 0fe8b95..81beff5 100644 --- a/packages/pic/package.json +++ b/packages/pic/package.json @@ -34,6 +34,6 @@ "@dfinity/identity": "^3.2.4", "@dfinity/principal": "^3.2.4", "bip39": "^3.1.0", - "json-with-bigint": "^3.4.4" + "json-with-bigint": "3.4.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2767c7d..9f95fca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: specifier: ^3.1.0 version: 3.1.0 json-with-bigint: - specifier: ^3.4.4 + specifier: 3.4.4 version: 3.4.4 packages/pic-server: {} From ade8b253a0be6f588991464d6bcca93568ade343 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 10 Dec 2025 11:31:45 +0100 Subject: [PATCH 3/6] fix: unneeded type conversion --- packages/pic/src/pocket-ic-client-types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pic/src/pocket-ic-client-types.ts b/packages/pic/src/pocket-ic-client-types.ts index d43dbe6..f01f29d 100644 --- a/packages/pic/src/pocket-ic-client-types.ts +++ b/packages/pic/src/pocket-ic-client-types.ts @@ -496,9 +496,9 @@ export function decodeGetControllersResponse( //#region GetTime -export type GetTimeResponse = { +export interface GetTimeResponse { nanosSinceEpoch: bigint; -}; +} export interface EncodedGetTimeResponse { nanos_since_epoch: bigint; From f3b037a66bafddea01382a4495d1909828d1180c Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 10 Dec 2025 11:44:06 +0100 Subject: [PATCH 4/6] fix!: use bigint for cycles --- packages/pic/src/pocket-ic-client-types.ts | 12 ++++++------ packages/pic/src/pocket-ic.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/pic/src/pocket-ic-client-types.ts b/packages/pic/src/pocket-ic-client-types.ts index f01f29d..0374a98 100644 --- a/packages/pic/src/pocket-ic-client-types.ts +++ b/packages/pic/src/pocket-ic-client-types.ts @@ -607,11 +607,11 @@ export function encodeGetCyclesBalanceRequest( } export interface EncodedGetCyclesBalanceResponse { - cycles: number; + cycles: bigint; } export interface GetCyclesBalanceResponse { - cycles: number; + cycles: bigint; } export function decodeGetCyclesBalanceResponse( @@ -628,12 +628,12 @@ export function decodeGetCyclesBalanceResponse( export interface AddCyclesRequest { canisterId: Principal; - amount: number; + amount: bigint; } export interface EncodedAddCyclesRequest { canister_id: string; - amount: number; + amount: bigint; } export function encodeAddCyclesRequest( @@ -646,11 +646,11 @@ export function encodeAddCyclesRequest( } export interface AddCyclesResponse { - cycles: number; + cycles: bigint; } export interface EncodedAddCyclesResponse { - cycles: number; + cycles: bigint; } export function decodeAddCyclesResponse( diff --git a/packages/pic/src/pocket-ic.ts b/packages/pic/src/pocket-ic.ts index 0b04321..01b0a2c 100644 --- a/packages/pic/src/pocket-ic.ts +++ b/packages/pic/src/pocket-ic.ts @@ -1226,7 +1226,7 @@ export class PocketIc { * await picServer.stop(); * ``` */ - public async getCyclesBalance(canisterId: Principal): Promise { + public async getCyclesBalance(canisterId: Principal): Promise { const { cycles } = await this.client.getCyclesBalance({ canisterId }); return cycles; @@ -1259,8 +1259,8 @@ export class PocketIc { */ public async addCycles( canisterId: Principal, - amount: number, - ): Promise { + amount: bigint, + ): Promise { const { cycles } = await this.client.addCycles({ canisterId, amount }); return cycles; From 5e1a331d5658a386db15072b2c4add26455aa8ca Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 10 Dec 2025 11:45:29 +0100 Subject: [PATCH 5/6] test: fix bigint --- examples/clock/tests/src/clock.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/clock/tests/src/clock.spec.ts b/examples/clock/tests/src/clock.spec.ts index 7e92a7e..fe489a3 100644 --- a/examples/clock/tests/src/clock.spec.ts +++ b/examples/clock/tests/src/clock.spec.ts @@ -71,7 +71,7 @@ describe('Clock', () => { it('should set and get canister cycles', async () => { const cycles = await pic.getCyclesBalance(canisterId); - const cyclesToAdd = 1_000_000_000; + const cyclesToAdd = BigInt(1_000_000_000); const updatedCyclesBalance = await pic.addCycles(canisterId, cyclesToAdd); const fetchUpdatedCyclesBalance = await pic.getCyclesBalance(canisterId); From 83ea2bcde1b8d180cf36a340198e457564665abd Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 10 Dec 2025 14:22:42 +0100 Subject: [PATCH 6/6] fix: override json parser --- packages/pic/src/http2-client.ts | 13 +++++++++---- packages/pic/src/pocket-ic-client.ts | 23 ++++++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/pic/src/http2-client.ts b/packages/pic/src/http2-client.ts index 0188251..249de19 100644 --- a/packages/pic/src/http2-client.ts +++ b/packages/pic/src/http2-client.ts @@ -1,4 +1,4 @@ -import { JSONParse, JSONStringify } from 'json-with-bigint'; +import { JSONStringify } from 'json-with-bigint'; import { ServerRequestTimeoutError } from './error'; import { isNil, poll } from './util'; @@ -14,12 +14,14 @@ export type RequestHeaders = RequestInit['headers']; export interface JsonGetRequest { path: string; headers?: RequestHeaders; + responseJsonParser?: typeof JSON.parse; } export interface JsonPostRequest { path: string; headers?: RequestHeaders; body?: B; + responseJsonParser?: typeof JSON.parse; } export type ResponseHeaders = ResponseInit['headers']; @@ -72,6 +74,7 @@ export class Http2Client { } public async jsonGet(init: JsonGetRequest): Promise { + const responseJsonParser = init.responseJsonParser ?? JSON.parse; // poll the request until it is successful or times out return await poll( async () => { @@ -81,7 +84,7 @@ export class Http2Client { headers: { ...init.headers, ...JSON_HEADER }, }); - const resBody = await getResBody(res); + const resBody = await getResBody(res, responseJsonParser); if (isNil(resBody)) { return resBody; } @@ -121,6 +124,7 @@ export class Http2Client { } public async jsonPost(init: JsonPostRequest): Promise { + const responseJsonParser = init.responseJsonParser ?? JSON.parse; const reqBody = init.body ? new TextEncoder().encode(JSONStringify(init.body)) : undefined; @@ -135,7 +139,7 @@ export class Http2Client { body: reqBody, }); - const resBody = await getResBody(res); + const resBody = await getResBody(res, responseJsonParser); if (isNil(resBody)) { return resBody; } @@ -201,10 +205,11 @@ export class Http2Client { async function getResBody( res: Response, + jsonParser: typeof JSON.parse, ): Promise> { const resBody = await res.text(); try { - return JSONParse(resBody) as ApiResponse; + return jsonParser(resBody) as ApiResponse; } catch (error) { const message = resBody; diff --git a/packages/pic/src/pocket-ic-client.ts b/packages/pic/src/pocket-ic-client.ts index c139844..94c4e66 100644 --- a/packages/pic/src/pocket-ic-client.ts +++ b/packages/pic/src/pocket-ic-client.ts @@ -1,3 +1,4 @@ +import { JSONParse } from 'json-with-bigint'; import { Http2Client } from './http2-client'; import { EncodedAddCyclesRequest, @@ -171,7 +172,10 @@ export class PocketIcClient { public async getTime(): Promise { this.assertInstanceNotDeleted(); - const res = await this.get('/read/get_time'); + const res = await this.get( + '/read/get_time', + JSONParse, + ); return decodeGetTimeResponse(res); } @@ -215,7 +219,7 @@ export class PocketIcClient { const res = await this.post< EncodedGetCyclesBalanceRequest, EncodedGetCyclesBalanceResponse - >('/read/get_cycles', encodeGetCyclesBalanceRequest(req)); + >('/read/get_cycles', encodeGetCyclesBalanceRequest(req), JSONParse); return decodeGetCyclesBalanceResponse(res); } @@ -226,7 +230,7 @@ export class PocketIcClient { const res = await this.post< EncodedAddCyclesRequest, EncodedAddCyclesResponse - >('/update/add_cycles', encodeAddCyclesRequest(req)); + >('/update/add_cycles', encodeAddCyclesRequest(req), JSONParse); return decodeAddCyclesResponse(res); } @@ -361,16 +365,25 @@ export class PocketIcClient { ); } - private async post(endpoint: string, body?: B): Promise { + private async post( + endpoint: string, + body?: B, + responseJsonParser?: typeof JSON.parse, + ): Promise { return await this.serverClient.jsonPost({ path: `${this.instancePath}${endpoint}`, body, + responseJsonParser, }); } - private async get(endpoint: string): Promise { + private async get( + endpoint: string, + responseJsonParser?: typeof JSON.parse, + ): Promise { return await this.serverClient.jsonGet({ path: `${this.instancePath}${endpoint}`, + responseJsonParser, }); }