From b119c8bc831f220ffcab5877b6d6d9e3a05db580 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 17:59:33 +0100 Subject: [PATCH 01/12] feat: max allowed latency to compute ack timeout --- src/ic-websocket.test.ts | 127 ++++++++++++++++++++++++++++++--------- src/ic-websocket.ts | 23 ++++--- 2 files changed, 113 insertions(+), 37 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 1fab560..6c4d079 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -4,7 +4,7 @@ import { setupServer } from "msw/node"; import { CallRequest, Cbor, fromHex } from "@dfinity/agent"; import { IDL } from "@dfinity/candid"; -import IcWebSocket, { createWsConfig } from "./ic-websocket"; +import IcWebSocket, { MAX_ALLOWED_NETWORK_LATENCY_MS, createWsConfig } from "./ic-websocket"; import { Principal } from "@dfinity/principal"; import { generateRandomIdentity } from "./identity"; import { CanisterWsMessageArguments, CanisterWsOpenArguments, ClientKey, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, isClientKeyEq, wsMessageIdl, wsOpenIdl } from "./idl"; @@ -425,102 +425,173 @@ describe("Messages acknowledgement", () => { }); afterEach(() => { + jest.useRealTimers(); mockWsServer.close(); }); - it("fails if messages are not acknowledged in time", async () => { - const ackMessageTimeoutMs = 2000; + it("fails if messages are never acknowledged", async () => { + const ackMessageIntervalMs = 2000; const icWsConfig = createWsConfig({ ...icWebsocketConfig, - ackMessageTimeout: ackMessageTimeoutMs, + ackMessageIntervalMs, }); const onError = jest.fn(); const onClose = jest.fn(); + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWsConfig); expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; + icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - // wait for the open message from the client + // wait for the ws_open message from the client await mockWsServer.nextMessage; + // send the open message to the client + mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); - const originalClientKey = { ...icWs["_clientKey"] }; - // workaround to simulate the client identity + // workaround: wait 100ms to make sure that + // the client processes the open message + await sleep(100); + + // when the client sends a message, it makes the ack timeout start, + // so here we have to mock the timers + jest.useFakeTimers(); + + // send a random application message from the client, + // so that the ack timeout starts + icWs.send({ text: "test" }); + + // wait for the second message from the client + await jest.advanceTimersToNextTimerAsync(); // needed just to advance the mockWsServer timeouts + await mockWsServer.nextMessage; + + // make the ack timeout expire + await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + MAX_ALLOWED_NETWORK_LATENCY_MS); + + const ackTimeoutError = new Error(`Ack message timeout. Not received ack for sequence numbers: ${[BigInt(1)]}`); + expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: ackTimeoutError })); + expect(onClose).toHaveBeenCalled(); + }); + + it("fails if messages are not acknowledged in time", async () => { + const ackMessageIntervalMs = 2000; + const icWsConfig = createWsConfig({ + ...icWebsocketConfig, + ackMessageIntervalMs, + }); + const onError = jest.fn(); + const onClose = jest.fn(); + + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWsConfig); + expect(icWs).toBeDefined(); + // workaround: simulate the client identity icWs["_clientKey"] = client1Key; - // send the open confirmation message from the canister + + icWs.onerror = onError; + icWs.onclose = onClose; + await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); + + // wait for the ws_open message from the client + await mockWsServer.nextMessage; + // send the open message to the client mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); + + // workaround: wait 100ms to make sure that + // the client processes the open message await sleep(100); - // set the client key back - icWs["_clientKey"] = originalClientKey; - // send a random application message + // when the client sends a message, it makes the ack timeout start, + // so here we have to mock the timers + jest.useFakeTimers(); + + // send a random application message from the client, + // so that the ack timeout starts icWs.send({ text: "test" }); // wait for the second message from the client + await jest.advanceTimersToNextTimerAsync(); // needed just to advance the mockWsServer timeouts await mockWsServer.nextMessage; - await sleep(ackMessageTimeoutMs); + // make the ack timeout expire + await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + MAX_ALLOWED_NETWORK_LATENCY_MS); + + // send the ack message from the canister + // when the ack timeout is already expired + mockWsServer.send(Cbor.encode(VALID_ACK_MESSAGE)); const ackTimeoutError = new Error(`Ack message timeout. Not received ack for sequence numbers: ${[BigInt(1)]}`); expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: ackTimeoutError })); - await sleep(10); expect(onClose).toHaveBeenCalled(); }); it("acknowledges messages", async () => { - const ackMessageTimeoutMs = 2000; + const ackMessageIntervalMs = 2000; const icWsConfig = createWsConfig({ ...icWebsocketConfig, - ackMessageTimeout: ackMessageTimeoutMs, + ackMessageIntervalMs, }); const onMessage = jest.fn(); const onError = jest.fn(); const onClose = jest.fn(); const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWsConfig); expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; + icWs.onmessage = onMessage; icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - // wait for the open message from the client + // wait for the ws_open message from the client await mockWsServer.nextMessage; - - const originalClientKey = { ...icWs["_clientKey"] }; - // workaround to simulate the client identity - icWs["_clientKey"] = client1Key; - // send the open confirmation message from the canister + // send the open message to the client mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); + + // workaround: wait 100ms to make sure that + // the client processes the open message await sleep(100); - // set the client key back - icWs["_clientKey"] = originalClientKey; + + // when the client sends a message, it makes the ack timeout start, + // so here we have to mock the timers + jest.useFakeTimers(); // send a random application message + // so that the ack timeout starts icWs.send({ text: "test" }); // wait for the second message from the client + await jest.advanceTimersToNextTimerAsync(); // needed just to advance the mockWsServer timeouts await mockWsServer.nextMessage; // send the ack message from the canister mockWsServer.send(Cbor.encode(VALID_ACK_MESSAGE)); - // wait until the ack timeout should expire - await sleep(ackMessageTimeoutMs); + console.log("sent ack message from canister"); - expect(onError).not.toHaveBeenCalled(); + // make the ack timeout expire + await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + MAX_ALLOWED_NETWORK_LATENCY_MS); + + // first message has been acknowledged correctly, + // as the error only reports the missing ack for the keep alive response + const ackTimeoutError = new Error(`Ack message timeout. Not received ack for sequence numbers: ${[BigInt(2)]}`); + expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: ackTimeoutError })); expect(onClose).not.toHaveBeenCalled(); // make sure onmessage is not called for service messages expect(onMessage).not.toHaveBeenCalled(); }); it("send an ack message after receiving the ack", async () => { - const ackMessageTimeoutMs = 2000; + const ackMessageIntervalMs = 2000; const icWsConfig = createWsConfig({ ...icWebsocketConfig, - ackMessageTimeout: ackMessageTimeoutMs, + ackMessageIntervalMs, }); const onMessage = jest.fn(); const onError = jest.fn(); diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index e48d966..f7a623e 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -36,10 +36,15 @@ import { import { WsAgent } from "./agent"; /** - * The default expiration time for receiving an ack message from the canister after sending a message. - * It's **3/2 times** the canister's default send ack period. + * The default interval (in milliseconds) at which the canister sends an ack message. */ -const DEFAULT_ACK_MESSAGE_TIMEOUT_MS = 450_000; +const DEFAULT_ACK_MESSAGE_INTERVAL_MS = 300_000; +/** + * The maximum latency allowed between the client and the canister. + * + * Used to determine the ack message timeout. + */ +export const MAX_ALLOWED_NETWORK_LATENCY_MS = 30_000; /** * Interface to create a new IcWebSocketConfig. For a simple configuration, use {@link createWsConfig}. @@ -62,12 +67,12 @@ export interface IcWebSocketConfig { */ networkUrl: string; /** - * The expiration (in milliseconds) time for receiving an ack message from the canister after sending a message. - * If the ack message is not received within this time, the connection will be closed. - * This parameter should always me **3/2 times or more** the canister's send ack period. - * @default 450_000 (7.5 minutes = 3/2 default send ack period on the canister) + * The interval (in milliseconds) at which the canister sends an ack message. + * This parameter must be **equal** to the canister's send ack interval. + * + * @default 300_000 (default send ack period on the canister) */ - ackMessageTimeout?: number; + ackMessageIntervalMs?: number; /** * The maximum age of the certificate received from the canister, in minutes. You won't likely need to set this parameter. Used in tests. * @@ -174,7 +179,7 @@ export default class IcWebSocket< }); this._ackMessagesQueue = new AckMessagesQueue({ - expirationMs: config.ackMessageTimeout || DEFAULT_ACK_MESSAGE_TIMEOUT_MS, + expirationMs: (config.ackMessageIntervalMs || DEFAULT_ACK_MESSAGE_INTERVAL_MS) + MAX_ALLOWED_NETWORK_LATENCY_MS, timeoutExpiredCallback: this._onAckMessageTimeout.bind(this), }); From d304b03fca92fa3afcadc6c3320287a472d1b36e Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 21:11:49 +0100 Subject: [PATCH 02/12] feat: timeout on connection opening --- src/ic-websocket.test.ts | 55 +++++++++++++++++++++++++++++++++++++--- src/ic-websocket.ts | 25 ++++++++++++++++++ src/utils.ts | 2 +- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 6c4d079..e5fbac4 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -201,6 +201,37 @@ describe("IcWebsocket class", () => { expect(icWs["_isConnectionEstablished"]).toEqual(false); }); + it("closes the connection if the open message is not received in time", async () => { + const onOpen = jest.fn(); + const onError = jest.fn(); + const onClose = jest.fn(); + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); + expect(icWs).toBeDefined(); + icWs.onopen = onOpen; + icWs.onerror = onError; + icWs.onclose = onClose; + await mockWsServer.connected; + + jest.useFakeTimers(); + mockWsServer.send(encodeHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY)); + + // advance the open timeout + await jest.advanceTimersByTimeAsync(2 * MAX_ALLOWED_NETWORK_LATENCY_MS); + + expect(icWs["_isConnectionEstablished"]).toEqual(false); + expect(onOpen).not.toHaveBeenCalled(); + const openError = new Error("Open timeout expired before receiving the open message"); + expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: openError })); + + await jest.runAllTimersAsync(); + await expect(mockWsServer.closed).resolves.not.toThrow(); + + expect(onClose).toHaveBeenCalled(); + expect(icWs.readyState).toEqual(WebSocket.CLOSED); + + jest.useRealTimers(); + }); + it("creates a new instance and sends the open message", async () => { const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); @@ -230,30 +261,46 @@ describe("IcWebsocket class", () => { it("onopen is called when open message from canister is received", async () => { const onOpen = jest.fn(); const onMessage = jest.fn(); + const onError = jest.fn(); const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; icWs.onopen = onOpen; icWs.onmessage = onMessage; + icWs.onerror = onError; await mockWsServer.connected; - await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); + + jest.useFakeTimers(); + mockWsServer.send(encodeHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY)); expect(onOpen).not.toHaveBeenCalled(); expect(icWs["_isConnectionEstablished"]).toEqual(false); + expect(onError).not.toHaveBeenCalled(); expect(onMessage).not.toHaveBeenCalled(); // wait for the open message from the client + await jest.advanceTimersToNextTimerAsync(); // needed just to advance the mockWsServer timeouts await mockWsServer.nextMessage; - // workaround to simulate the client identity - icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); - await sleep(100); + jest.runAllTicks(); + + // advance the open timeout so that it expires + // workaround: call the advanceTimers twice + // to make message processing happening in the meantime + await jest.advanceTimersByTimeAsync(MAX_ALLOWED_NETWORK_LATENCY_MS); + await jest.advanceTimersByTimeAsync(MAX_ALLOWED_NETWORK_LATENCY_MS); expect(onOpen).toHaveBeenCalled(); expect(icWs["_isConnectionEstablished"]).toEqual(true); + expect(onError).not.toHaveBeenCalled(); + expect(icWs.readyState).toEqual(WebSocket.OPEN); // make sure onmessage callback is not called when receiving the first message expect(onMessage).not.toHaveBeenCalled(); + + jest.useRealTimers(); }); it("onmessage is called when a valid message is received", async () => { diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index f7a623e..86dfc0b 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -109,6 +109,7 @@ export default class IcWebSocket< private _clientKey: ClientKey; private _gatewayPrincipal: Principal | null = null; private _maxCertificateAgeInMinutes = 5; + private _openTimeout: NodeJS.Timeout | null = null; onclose: ((this: IcWebSocket, ev: CloseEvent) => any) | null = null; onerror: ((this: IcWebSocket, ev: ErrorEvent) => any) | null = null; @@ -231,6 +232,27 @@ export default class IcWebSocket< this._incomingMessagesQueue.addAndProcess(event.data); } + private _startOpenTimeout() { + // the timeout is double the maximum allowed network latency, + // because opening the connection involves a message sent by the client and one by the canister + this._openTimeout = setTimeout(() => { + if (!this._isConnectionEstablished) { + logger.error("[onWsOpen] Error: Open timeout expired before receiving the open message"); + this._callOnErrorCallback(new Error("Open timeout expired before receiving the open message")); + this._wsInstance.close(4000, "Open connection timeout"); + } + + this._openTimeout = null; + }, 2 * MAX_ALLOWED_NETWORK_LATENCY_MS); + } + + private _cancelOpenTimeout() { + if (this._openTimeout) { + clearTimeout(this._openTimeout); + this._openTimeout = null; + } + } + private async _handleHandshakeMessage(handshakeMessage: GatewayHandshakeMessage): Promise { // at this point, we're sure that the gateway_principal is valid // because the isGatewayHandshakeMessage function checks it @@ -239,6 +261,8 @@ export default class IcWebSocket< try { await this._sendOpenMessage(); + + this._startOpenTimeout(); } catch (error) { logger.error("[onWsMessage] Handshake message error:", error); // if a handshake message fails, we can't continue @@ -340,6 +364,7 @@ export default class IcWebSocket< } this._isConnectionEstablished = true; + this._cancelOpenTimeout(); this._callOnOpenCallback(); diff --git a/src/utils.ts b/src/utils.ts index f4f5090..165893a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -84,7 +84,7 @@ export const safeExecute = async ( warnMessage: string ): Promise => { try { - return await fn(); + return await Promise.resolve(fn()); } catch (error) { logger.warn(warnMessage, error); } From bacd12e9d58a0da2716af1de8874bf8d036db94d Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 21:25:59 +0100 Subject: [PATCH 03/12] fix: timeoutExpiredCallback with all expired items --- src/queues.test.ts | 12 ++++++------ src/queues.ts | 11 ++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/queues.test.ts b/src/queues.test.ts index 10cd008..486ebc8 100644 --- a/src/queues.test.ts +++ b/src/queues.test.ts @@ -5,7 +5,7 @@ describe("BaseQueue", () => { beforeEach(() => { queue = new BaseQueue({ - itemCallback: (message: string) => true, + itemCallback: (_: string) => true, }); }); @@ -121,6 +121,7 @@ describe("AckMessagesQueue", () => { const expirationMs = 1000; beforeEach(() => { + jest.useFakeTimers(); queue = new AckMessagesQueue({ expirationMs, timeoutExpiredCallback: jest.fn(), @@ -140,7 +141,7 @@ describe("AckMessagesQueue", () => { }); it("should call the timeoutExpiredCallback for expired items when not receiving any ack", () => { - jest.useFakeTimers().setSystemTime(Date.now() + expirationMs + 1); + jest.setSystemTime(Date.now() + expirationMs + 1); queue.add(BigInt(1)); jest.advanceTimersByTime(expirationMs + 1); expect(queue.last()).toBeNull(); @@ -170,19 +171,18 @@ describe("AckMessagesQueue", () => { it("should call the timeoutExpiredCallback for expired items when receiving the ack", () => { queue.add(BigInt(1)); - jest.useFakeTimers().setSystemTime(Date.now() + expirationMs + 1); queue.add(BigInt(2)); + queue.add(BigInt(3)); + jest.setSystemTime(Date.now() + expirationMs + 1); queue.ack(BigInt(1)); - jest.advanceTimersByTime(expirationMs + 1); expect(queue.last()).toBeNull(); - expect(queue["_timeoutExpiredCallback"]).toHaveBeenCalledWith([BigInt(2)]); + expect(queue["_timeoutExpiredCallback"]).toHaveBeenCalledWith([BigInt(2), BigInt(3)]); }); it("should call the timeoutExpiredCallback for all expired items after not receiving the ack", () => { queue.add(BigInt(1)); queue.add(BigInt(2)); queue.add(BigInt(3)); - jest.useFakeTimers(); queue.ack(BigInt(1)); jest.advanceTimersByTime(expirationMs); expect(queue.last()).toBeNull(); diff --git a/src/queues.ts b/src/queues.ts index a8e4683..9b9dd39 100644 --- a/src/queues.ts +++ b/src/queues.ts @@ -173,13 +173,10 @@ export class AckMessagesQueue { } // for the remaining items in the queue, check if they have expired - // if yes, call the callback for the first expired item - for (const item of this._queue) { - if (Date.now() - item.addedAt >= this._expirationMs) { - // if it has expired and is still in the queue, - // it means it has not been acked, so we call the callback - return this._onTimeoutExpired([item]); - } + // if yes, call the callback for the expired items + const expiredItems = this._queue.filter((item) => Date.now() - item.addedAt >= this._expirationMs); + if (expiredItems.length > 0) { + return this._onTimeoutExpired(expiredItems); } this._restartLastAckTimeout(); From 90101bb017e5ab2683e5909cd8fbe507f0fda043 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 21:26:31 +0100 Subject: [PATCH 04/12] fix; useRealTimers when finishing each test --- src/ic-websocket.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index e5fbac4..c75c422 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -55,6 +55,7 @@ describe("IcWebsocket class", () => { }); afterEach(() => { + jest.useRealTimers(); mockWsServer.close(); }); @@ -228,8 +229,6 @@ describe("IcWebsocket class", () => { expect(onClose).toHaveBeenCalled(); expect(icWs.readyState).toEqual(WebSocket.CLOSED); - - jest.useRealTimers(); }); it("creates a new instance and sends the open message", async () => { @@ -299,8 +298,6 @@ describe("IcWebsocket class", () => { expect(icWs.readyState).toEqual(WebSocket.OPEN); // make sure onmessage callback is not called when receiving the first message expect(onMessage).not.toHaveBeenCalled(); - - jest.useRealTimers(); }); it("onmessage is called when a valid message is received", async () => { From bcf069480372b147614fbf8d0c2b79265d8c5f91 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 21:45:09 +0100 Subject: [PATCH 05/12] chore: fix tests by awaiting promises see https://github.com/jestjs/jest/issues/2157#issuecomment-897935688 --- src/ic-websocket.test.ts | 10 +++------- src/test/helpers.ts | 4 ++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index c75c422..81e6640 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -10,7 +10,7 @@ import { generateRandomIdentity } from "./identity"; import { CanisterWsMessageArguments, CanisterWsOpenArguments, ClientKey, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, isClientKeyEq, wsMessageIdl, wsOpenIdl } from "./idl"; import { canisterId, client1Key } from "./test/clients"; import { INVALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, INVALID_MESSAGE_KEY, VALID_ACK_MESSAGE, VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, VALID_MESSAGE_SEQ_NUM_2, VALID_MESSAGE_SEQ_NUM_3, VALID_OPEN_MESSAGE, encodeHandshakeMessage } from "./test/messages"; -import { sleep } from "./test/helpers"; +import { flushPromises, sleep } from "./test/helpers"; import { getTestCanisterActor, getTestCanisterActorWithoutMethods, getTestCanisterActorWrongArgs, getTestCanisterActorWrongOpt } from "./test/actor"; import type { WsAgentRequestMessage } from "./agent/types"; import { GATEWAY_PRINCIPAL } from "./test/constants"; @@ -226,7 +226,6 @@ describe("IcWebsocket class", () => { await jest.runAllTimersAsync(); await expect(mockWsServer.closed).resolves.not.toThrow(); - expect(onClose).toHaveBeenCalled(); expect(icWs.readyState).toEqual(WebSocket.CLOSED); }); @@ -284,13 +283,10 @@ describe("IcWebsocket class", () => { // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); - jest.runAllTicks(); // advance the open timeout so that it expires - // workaround: call the advanceTimers twice - // to make message processing happening in the meantime - await jest.advanceTimersByTimeAsync(MAX_ALLOWED_NETWORK_LATENCY_MS); - await jest.advanceTimersByTimeAsync(MAX_ALLOWED_NETWORK_LATENCY_MS); + await flushPromises(); // make the message processing happen + await jest.advanceTimersByTimeAsync(2 * MAX_ALLOWED_NETWORK_LATENCY_MS); expect(onOpen).toHaveBeenCalled(); expect(icWs["_isConnectionEstablished"]).toEqual(true); diff --git a/src/test/helpers.ts b/src/test/helpers.ts index fe57bb6..08be178 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1 +1,5 @@ export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function flushPromises(): Promise { + return new Promise(jest.requireActual("timers").setImmediate); +}; From 81e971fe0214bc881988932db32c02ebbb37d2ef Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 22:06:08 +0100 Subject: [PATCH 06/12] chore: unnecessary client key copying --- src/ic-websocket.test.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 81e6640..d3aff37 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -301,19 +301,16 @@ describe("IcWebsocket class", () => { const onError = jest.fn(); const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; icWs.onmessage = onMessage; icWs.onerror = onError; await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - const originalClientKey = { ...icWs["_clientKey"] }; - // workaround to simulate the client identity - icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); await sleep(100); - // set the client key back - icWs["_clientKey"] = originalClientKey; expect(onMessage).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); @@ -334,20 +331,17 @@ describe("IcWebsocket class", () => { const onClose = jest.fn(); const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; icWs.onmessage = onMessage; icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - const originalClientKey = { ...icWs["_clientKey"] }; - // workaround to simulate the client identity - icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); await sleep(100); - // set the client key back - icWs["_clientKey"] = originalClientKey; expect(onMessage).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); @@ -370,20 +364,17 @@ describe("IcWebsocket class", () => { const onClose = jest.fn(); const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; icWs.onmessage = onMessage; icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - const originalClientKey = { ...icWs["_clientKey"] }; - // workaround to simulate the client identity - icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); await sleep(100); - // set the client key back - icWs["_clientKey"] = originalClientKey; expect(onMessage).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); @@ -418,6 +409,9 @@ describe("IcWebsocket class", () => { // wait for the open message from the client await mockWsServer.nextMessage; + // we can't use the same worksaround as in the previous tests + // because here we need to check the message sent to the canister, + // which needs a real signature const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; @@ -482,7 +476,6 @@ describe("Messages acknowledgement", () => { expect(icWs).toBeDefined(); // workaround: simulate the client identity icWs["_clientKey"] = client1Key; - icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; @@ -530,7 +523,6 @@ describe("Messages acknowledgement", () => { expect(icWs).toBeDefined(); // workaround: simulate the client identity icWs["_clientKey"] = client1Key; - icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; @@ -582,7 +574,6 @@ describe("Messages acknowledgement", () => { expect(icWs).toBeDefined(); // workaround: simulate the client identity icWs["_clientKey"] = client1Key; - icWs.onmessage = onMessage; icWs.onerror = onError; icWs.onclose = onClose; @@ -647,6 +638,9 @@ describe("Messages acknowledgement", () => { // wait for the open message from the client await mockWsServer.nextMessage; + // we can't use the same worksaround as in the previous tests + // because here we need to check the message sent to the canister, + // which needs a real signature const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; From b45615726a1ee42ecf49992c1d43c7bc80602697 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 22:23:53 +0100 Subject: [PATCH 07/12] feat: close conn. when receiving close message wip: needs tests --- src/ic-websocket.ts | 16 ++++++++++++++++ src/idl.ts | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index 86dfc0b..e3347fd 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -9,6 +9,7 @@ import { IDL } from "@dfinity/candid"; import { Principal } from "@dfinity/principal"; import { CanisterAckMessageContent, + CanisterCloseMessageContent, CanisterWsMessageArguments, ClientKeepAliveMessageContent, ClientKey, @@ -371,6 +372,10 @@ export default class IcWebSocket< this._outgoingMessagesQueue.enableAndProcess(); } else if ("AckMessage" in serviceMessage) { await this._handleAckMessageFromCanister(serviceMessage.AckMessage); + } else if ("CloseMessage" in serviceMessage) { + await this._handleCloseMessageFromCanister(serviceMessage.CloseMessage); + // we don't have to process any further message (there shouldn't be any anyway) + return false; } else { throw new Error("Invalid service message from canister"); } @@ -399,6 +404,17 @@ export default class IcWebSocket< await this._sendKeepAliveMessage(); } + private async _handleCloseMessageFromCanister(content: CanisterCloseMessageContent): Promise { + if ("ClosedByApplication" in content.reason) { + logger.debug("[onWsMessage] Received close message from canister. Reason: ClosedByApplication"); + this._wsInstance.close(4001, "ClosedByApplication"); + } else { + logger.error("[onWsMessage] Received close message from canister. Reason:", content.reason); + this._callOnErrorCallback(new Error(`Received close message from canister. Reason: ${content.reason}`)); + this._wsInstance.close(4000, "Received close message from canister"); + } + } + private async _sendKeepAliveMessage(): Promise { const keepAliveMessageContent: ClientKeepAliveMessageContent = { last_incoming_sequence_num: this._incomingSequenceNum - BigInt(1), diff --git a/src/idl.ts b/src/idl.ts index ba0ac5a..4336330 100644 --- a/src/idl.ts +++ b/src/idl.ts @@ -78,12 +78,26 @@ export type CanisterAckMessageContent = { export type ClientKeepAliveMessageContent = { 'last_incoming_sequence_num': bigint, }; +export type CloseMessageReason = { + WrongSequenceNumber: null, +} | { + InvalidServiceMessage: null, +} | { + KeepAliveTimeout: null, +} | { + ClosedByApplication: null +}; +export type CanisterCloseMessageContent = { + reason: CloseMessageReason, +}; export type WebsocketServiceMessageContent = { OpenMessage: CanisterOpenMessageContent, } | { AckMessage: CanisterAckMessageContent, } | { KeepAliveMessage: ClientKeepAliveMessageContent, +} | { + CloseMessage: CanisterCloseMessageContent, }; const CanisterOpenMessageContentIdl = IDL.Record({ @@ -95,10 +109,20 @@ const CanisterAckMessageContentIdl = IDL.Record({ const ClientKeepAliveMessageContentIdl = IDL.Record({ 'last_incoming_sequence_num': IDL.Nat64, }); +const CloseMessageReasonIdl = IDL.Variant({ + 'WrongSequenceNumber': IDL.Null, + 'InvalidServiceMessage': IDL.Null, + 'KeepAliveTimeout': IDL.Null, + 'ClosedByApplication': IDL.Null, +}); +const CanisterCloseMessageContentIdl = IDL.Record({ + 'reason': CloseMessageReasonIdl, +}) const WebsocketServiceMessageContentIdl = IDL.Variant({ 'OpenMessage': CanisterOpenMessageContentIdl, 'AckMessage': CanisterAckMessageContentIdl, 'KeepAliveMessage': ClientKeepAliveMessageContentIdl, + 'CloseMessage': CanisterCloseMessageContentIdl, }); export const decodeWebsocketServiceMessageContent = (bytes: Uint8Array): WebsocketServiceMessageContent => { From 40fe5f3a8f2b7f73c3901f1a4ea86aae0a5b517f Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 22:31:16 +0100 Subject: [PATCH 08/12] chore: fix tests by using real timers --- src/ic-websocket.test.ts | 15 ++++++++------- src/test/helpers.ts | 4 ---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index d3aff37..22f3de5 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -10,7 +10,7 @@ import { generateRandomIdentity } from "./identity"; import { CanisterWsMessageArguments, CanisterWsOpenArguments, ClientKey, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, isClientKeyEq, wsMessageIdl, wsOpenIdl } from "./idl"; import { canisterId, client1Key } from "./test/clients"; import { INVALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, INVALID_MESSAGE_KEY, VALID_ACK_MESSAGE, VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, VALID_MESSAGE_SEQ_NUM_2, VALID_MESSAGE_SEQ_NUM_3, VALID_OPEN_MESSAGE, encodeHandshakeMessage } from "./test/messages"; -import { flushPromises, sleep } from "./test/helpers"; +import { sleep } from "./test/helpers"; import { getTestCanisterActor, getTestCanisterActorWithoutMethods, getTestCanisterActorWrongArgs, getTestCanisterActorWrongOpt } from "./test/actor"; import type { WsAgentRequestMessage } from "./agent/types"; import { GATEWAY_PRINCIPAL } from "./test/constants"; @@ -256,6 +256,8 @@ describe("IcWebsocket class", () => { }); }); + // we can't use fake timers here, because we need to wait for the processing of the open message + // TODO: figure out how to fix this, since it increases a lot the test duration it("onopen is called when open message from canister is received", async () => { const onOpen = jest.fn(); const onMessage = jest.fn(); @@ -269,7 +271,6 @@ describe("IcWebsocket class", () => { icWs.onerror = onError; await mockWsServer.connected; - jest.useFakeTimers(); mockWsServer.send(encodeHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY)); expect(onOpen).not.toHaveBeenCalled(); @@ -278,15 +279,15 @@ describe("IcWebsocket class", () => { expect(onMessage).not.toHaveBeenCalled(); // wait for the open message from the client - await jest.advanceTimersToNextTimerAsync(); // needed just to advance the mockWsServer timeouts await mockWsServer.nextMessage; // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); + // wait for the message to be processed + await sleep(100); - // advance the open timeout so that it expires - await flushPromises(); // make the message processing happen - await jest.advanceTimersByTimeAsync(2 * MAX_ALLOWED_NETWORK_LATENCY_MS); + // wait for the open timeout so that it expires + await sleep(2 * MAX_ALLOWED_NETWORK_LATENCY_MS); expect(onOpen).toHaveBeenCalled(); expect(icWs["_isConnectionEstablished"]).toEqual(true); @@ -294,7 +295,7 @@ describe("IcWebsocket class", () => { expect(icWs.readyState).toEqual(WebSocket.OPEN); // make sure onmessage callback is not called when receiving the first message expect(onMessage).not.toHaveBeenCalled(); - }); + }, 3 * MAX_ALLOWED_NETWORK_LATENCY_MS); it("onmessage is called when a valid message is received", async () => { const onMessage = jest.fn(); diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 08be178..fe57bb6 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1,5 +1 @@ export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export function flushPromises(): Promise { - return new Promise(jest.requireActual("timers").setImmediate); -}; From e49876df0e73511fe2023f42654930284c0d1ad3 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 8 Dec 2023 22:33:58 +0100 Subject: [PATCH 09/12] chore: link to not working workaround --- src/ic-websocket.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 22f3de5..a00920e 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -257,6 +257,8 @@ describe("IcWebsocket class", () => { }); // we can't use fake timers here, because we need to wait for the processing of the open message + // the workaround suggested at https://github.com/jestjs/jest/issues/2157#issuecomment-897935688 + // doesn't seem to work deterministically // TODO: figure out how to fix this, since it increases a lot the test duration it("onopen is called when open message from canister is received", async () => { const onOpen = jest.fn(); From 28288a5679c6e760bf42ca981bd937dd3c23c559 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Sat, 9 Dec 2023 19:07:56 +0100 Subject: [PATCH 10/12] chore: test the close message logic --- src/ic-websocket.test.ts | 101 ++++++++++++++++++++++++++++++--------- src/test/clients.ts | 6 +-- src/test/constants.ts | 5 +- src/test/messages.ts | 50 +++++++++++-------- 4 files changed, 116 insertions(+), 46 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index a00920e..f6cc66d 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -1,20 +1,44 @@ import WsMockServer from "jest-websocket-mock"; import { rest } from "msw"; import { setupServer } from "msw/node"; -import { CallRequest, Cbor, fromHex } from "@dfinity/agent"; +import { CallRequest, Cbor } from "@dfinity/agent"; import { IDL } from "@dfinity/candid"; +import { Principal } from "@dfinity/principal"; import IcWebSocket, { MAX_ALLOWED_NETWORK_LATENCY_MS, createWsConfig } from "./ic-websocket"; -import { Principal } from "@dfinity/principal"; import { generateRandomIdentity } from "./identity"; -import { CanisterWsMessageArguments, CanisterWsOpenArguments, ClientKey, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, isClientKeyEq, wsMessageIdl, wsOpenIdl } from "./idl"; +import { + CanisterWsMessageArguments, + CanisterWsOpenArguments, + ClientKey, + WebsocketServiceMessageContent, + _WS_CANISTER_SERVICE, + decodeWebsocketServiceMessageContent, + wsMessageIdl, + wsOpenIdl, +} from "./idl"; +import { GatewayHandshakeMessage } from "./types"; +import type { WsAgentRequestMessage } from "./agent/types"; + import { canisterId, client1Key } from "./test/clients"; -import { INVALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, INVALID_MESSAGE_KEY, VALID_ACK_MESSAGE, VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, VALID_MESSAGE_SEQ_NUM_2, VALID_MESSAGE_SEQ_NUM_3, VALID_OPEN_MESSAGE, encodeHandshakeMessage } from "./test/messages"; +import { + INVALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, + INVALID_MESSAGE_KEY, + VALID_ACK_MESSAGE, + VALID_CLOSE_MESSAGE, + VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY, + VALID_MESSAGE_SEQ_NUM_3, + VALID_OPEN_MESSAGE, + encodeHandshakeMessage, +} from "./test/messages"; import { sleep } from "./test/helpers"; -import { getTestCanisterActor, getTestCanisterActorWithoutMethods, getTestCanisterActorWrongArgs, getTestCanisterActorWrongOpt } from "./test/actor"; -import type { WsAgentRequestMessage } from "./agent/types"; -import { GATEWAY_PRINCIPAL } from "./test/constants"; -import { GatewayHandshakeMessage } from "./types"; +import { + getTestCanisterActor, + getTestCanisterActorWithoutMethods, + getTestCanisterActorWrongArgs, + getTestCanisterActorWrongOpt, +} from "./test/actor"; +import { GATEWAY_PRINCIPAL, LOCAL_REPLICA_ROOT_KEY } from "./test/constants"; const wsGatewayAddress = "ws://127.0.0.1:8080"; // the canister from which the application message was sent (needed to verify the message certificate) @@ -38,7 +62,7 @@ const mockReplica = setupServer( ctx.status(200), // this response was generated from the same local replica // used to generate the messages below - ctx.body(fromHex("d9d9f7a66e69635f6170695f76657273696f6e66302e31382e3068726f6f745f6b65795885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100948a091fa3439c49aa8782da536348bba3a525cc0b63c0e202797ae7baf38f615e5375b694818b4a1a5b0fb07242aede15eb79f6454c19c1ee54fd8b9c14dbb06d94df2f2a3cc4f6336f0419680025f4411f0d764aa0b6e9fd246ba71a80fad66c696d706c5f76657273696f6e65302e382e3069696d706c5f68617368784030343366663064393237626337313431643761643630616235646331313934636364303164393761386431633333393632643236663730323461646463336135757265706c6963615f6865616c74685f737461747573676865616c746879706365727469666965645f686569676874181b")), + ctx.body(LOCAL_REPLICA_ROOT_KEY), ); }), ); @@ -314,16 +338,16 @@ describe("IcWebsocket class", () => { // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); await sleep(100); + expect(onMessage).not.toHaveBeenCalled(); + // send the ack message that has sequence number 2 + mockWsServer.send(Cbor.encode(VALID_ACK_MESSAGE)); + await sleep(100); expect(onMessage).not.toHaveBeenCalled(); - expect(onError).not.toHaveBeenCalled(); // send an application message from the canister - mockWsServer.send(Cbor.encode(VALID_MESSAGE_SEQ_NUM_2)); - - // wait for the message to be processed + mockWsServer.send(Cbor.encode(VALID_MESSAGE_SEQ_NUM_3)); await sleep(100); - expect(onMessage).toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); }); @@ -345,16 +369,12 @@ describe("IcWebsocket class", () => { // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); await sleep(100); - expect(onMessage).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); // send an application message from the canister mockWsServer.send(Cbor.encode(VALID_MESSAGE_SEQ_NUM_3)); - - // wait for the message to be processed await sleep(100); - expect(onMessage).not.toHaveBeenCalled(); const seqNumError = new Error("[onWsMessage] Received message sequence number does not match next expected value. Expected: 2, received: 3"); expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: new Error(`Error receiving message: ${seqNumError}`) })); @@ -378,16 +398,12 @@ describe("IcWebsocket class", () => { // send the open confirmation message from the canister mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); await sleep(100); - expect(onMessage).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); // send an application message from the canister mockWsServer.send(Cbor.encode(INVALID_MESSAGE_KEY)); - - // wait for the message to be processed await sleep(100); - expect(onMessage).not.toHaveBeenCalled(); const invalidCertificateError = new Error("[onWsMessage] Certificate validation failed"); expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: new Error(`Error receiving message: ${invalidCertificateError}`) })); @@ -454,6 +470,47 @@ describe("IcWebsocket class", () => { expect(IDL.decode([IDL.Record({ 'text': IDL.Text })], wsMessageArgs[0].msg.content as Uint8Array)[0]).toMatchObject(applicationMessageContent); expect(wsMessageArgs[1]).toEqual([]); // check that we're not sending unneeded arguments }); + + it("closes the connection if close message is received", async () => { + const onMessage = jest.fn(); + const onError = jest.fn(); + const onClose = jest.fn(); + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); + expect(icWs).toBeDefined(); + // workaround: simulate the client identity + icWs["_clientKey"] = client1Key; + icWs.onmessage = onMessage; + icWs.onerror = onError; + icWs.onclose = onClose; + await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); + + // we need to send the previous messages in order + // to not break the sequence number count + + // send the open confirmation message from the canister + mockWsServer.send(Cbor.encode(VALID_OPEN_MESSAGE)); + await sleep(100); + expect(onClose).not.toHaveBeenCalled(); + + // send the ack message that has sequence number 2 + mockWsServer.send(Cbor.encode(VALID_ACK_MESSAGE)); + await sleep(100); + expect(onClose).not.toHaveBeenCalled(); + + // send an application message from the canister + mockWsServer.send(Cbor.encode(VALID_MESSAGE_SEQ_NUM_3)); + await sleep(100); + expect(onClose).not.toHaveBeenCalled(); + + // finally, send the close message + mockWsServer.send(Cbor.encode(VALID_CLOSE_MESSAGE)); + await sleep(100); + expect(onMessage).toHaveBeenCalledTimes(1); // only with the application message + expect(onError).not.toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); + await expect(mockWsServer.closed).resolves.not.toThrow(); + }); }); describe("Messages acknowledgement", () => { diff --git a/src/test/clients.ts b/src/test/clients.ts index 8891358..1b7dca9 100644 --- a/src/test/clients.ts +++ b/src/test/clients.ts @@ -3,9 +3,7 @@ import { Principal } from "@dfinity/principal"; export const canisterId = Principal.fromText("bnz7o-iuaaa-aaaaa-qaaaa-cai"); -// Principal: "pmisz-prtlk-b6oe6-bj4fl-6l5fy-h7c2h-so6i7-jiz2h-bgto7-piqfr-7ae" -// const client1Seed = "rabbit fun moral twin food kangaroo egg among adjust pottery measure seek"; export const client1Key: ClientKey = { - client_principal: Principal.fromText("pmisz-prtlk-b6oe6-bj4fl-6l5fy-h7c2h-so6i7-jiz2h-bgto7-piqfr-7ae"), - client_nonce: BigInt("5768810803147064100"), + client_principal: Principal.fromText("kj67s-b5v2y-ahlkr-kmume-xbow6-zwbtj-j4j3m-ae46e-qqrcu-uxiby-yae"), + client_nonce: BigInt("385892949151814926"), }; diff --git a/src/test/constants.ts b/src/test/constants.ts index 828e636..7630c4e 100644 --- a/src/test/constants.ts +++ b/src/test/constants.ts @@ -1,3 +1,6 @@ +import { fromHex } from "@dfinity/agent"; import { Principal } from "@dfinity/principal"; -export const GATEWAY_PRINCIPAL = Principal.fromText("i3gux-m3hwt-5mh2w-t7wwm-fwx5j-6z6ht-hxguo-t4rfw-qp24z-g5ivt-2qe"); \ No newline at end of file +export const GATEWAY_PRINCIPAL = Principal.fromText("sqdfl-mr4km-2hfjy-gajqo-xqvh7-hf4mf-nra4i-3it6l-neaw4-soolw-tae"); + +export const LOCAL_REPLICA_ROOT_KEY = fromHex("d9d9f7a66e69635f6170695f76657273696f6e66302e31382e3068726f6f745f6b65795885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361008005229d89a17c6f9ec403a4b1a8aa103fc48055046c95f1e60ee2fbfb0bb23ab21617a93f48b99b1199ac89008cf3cf0a83e9da35f5cf27d0d51535ceff89c43ee236c31c3a7865cc6b333194ad3f7155b2931a7ffec2066777dffb20f277ca6c696d706c5f76657273696f6e65302e382e3069696d706c5f68617368784064613931633732316637386462393433346561336630303437383939383836346439313731346538626561363862333963633736326662306263383937313662757265706c6963615f6865616c74685f737461747573676865616c746879706365727469666965645f68656967687418d4"); diff --git a/src/test/messages.ts b/src/test/messages.ts index c6f392c..9aa4133 100644 --- a/src/test/messages.ts +++ b/src/test/messages.ts @@ -1,3 +1,9 @@ +/** + * These messages have been generated by running a simple canister in a local replica. + * + * TODO: find a better and more robust way to generate these messages + */ + import { Cbor, fromHex } from "@dfinity/agent"; import { ClientIncomingMessage, GatewayHandshakeMessage } from "../types"; import { GATEWAY_PRINCIPAL } from "./constants"; @@ -16,38 +22,44 @@ export const encodeHandshakeMessage = (message: GatewayHandshakeMessage): ArrayB return Cbor.encode(message); } -// Messages generated from a canister running in a local replica +// sequence_num: 1 export const VALID_OPEN_MESSAGE: ClientIncomingMessage = { key: `${GATEWAY_PRINCIPAL}_00000000000000000000`, - content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651b500eeedc6a0443246c73657175656e63655f6e756d016974696d657374616d701b178e8b8e33b7a68b7269735f736572766963655f6d657373616765f567636f6e74656e7458614449444c046b03fdbd95cc0101bfd397b409039eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a78010000011d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e022443046adcee0e50")), - cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f6461746182035820837cecb72b1d6f835b4afebdfa25c0f9363fc60ef76a8f1ff8faab3643a85d4a82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820c57ba057d4a62e4c0c87281acc681df06dd4e662ad70659f2034b6fd84ac385882045820f50b1aca22549e65eb4bc3a8cab4863e1ee207542d1bcb63eeb2756b7cd4230a830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d65820349e9b0edc7e5f1a2c717697369676e61747572655830aa7b6219625b204b5f5f9d1642128799cd3cca476e4dcb39594f7f88df51233e6576ba4404914da8e33dc018c0b374ea")), - tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b65748302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303082035820049a01f5aff5584d8350c83c793a1e645b55ba595b4aceb307266823db0525fa")), + content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581db5d60075aa2a65184b85d6f66c19a53c4ed80273c484222a52e80e30026c636c69656e745f6e6f6e63651b055af797bb85950e6c73657175656e63655f6e756d016974696d657374616d701b179f3ab61f182c6c7269735f736572766963655f6d657373616765f567636f6e74656e7458894449444c066b04fdbd95cc0101bfd397b409038ff2d1ef0a049eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a786c01c49ff4e40f056b04cdc2dabb047fc9c68ea0057fd7aba29c0a7f999dafab0d7f010000011db5d60075aa2a65184b85d6f66c19a53c4ed80273c484222a52e80e30020e9585bb97f75a05")), + cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a800000000010000001018301830183024e6365727469666965645f6461746182035820e17d2f5533250b07d0cac69dd789e9b123aabf7663e3f91817567a0fe789a3028204582073cfb1430ef73ab885c27fd576cd8baba82d936234c18a872e75a690c6369be782045820033ceb4a3ee94c0447ef8c00e1901f5c168c6230684f759c98a13dc47a921bed8204582085e48bc6d000256b6d92382c12e88b3143418269dce4335a3a7c93041eca86d782045820f45245b933d677e216d0aeef13549138b6fdb775f2eaa1fa19f0a160a230131082045820c63979474a02d2e05c5007ca926d4bdc78c063a648f6295935cbb4154f294058830182045820c6cb372d626abbdde6f455d99734ed4fba658753d6907f121a0bb01ab67e9ea883024474696d65820349ecd8e0f8e1d6cecf17697369676e617475726558308cb526c30c54b2e9ff59bbd4175c4af2d379dabd2583d6202c8a2ef60526e138db66c70cc32012b0050b12fb4de7f04e")), + tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b657483025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f30303030303030303030303030303030303030308203582017591ba8714082846a76a1709b8fe042c894ce2f371a5e1c68907afe88c0ba5e")), }; -export const VALID_MESSAGE_SEQ_NUM_2: ClientIncomingMessage = { +// acks message received from client with sequence number 1 +// sequence_num: 2 +export const VALID_ACK_MESSAGE: ClientIncomingMessage = { key: `${GATEWAY_PRINCIPAL}_00000000000000000001`, - content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651bf39dfc3f05b6b1a86c73657175656e63655f6e756d026974696d657374616d701b178e91934f3b735b7269735f736572766963655f6d657373616765f467636f6e74656e74554449444c016c01ad99e7e704710100057465737430")), - cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f646174618203582082e4f0e1f3117e09785f1b5568f0bd7f704a6ea0bd7d79757092990747c4595e82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820bf11fc78a00eb40fcceb76e1b4c55f056c4fe6eaf7af4b4e7525e0c489f61a4882045820aca43161972742f98498c19102837f47510e88151eb257f70ca7515d7b6d255e830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d658203499bc5e7a4b7b2a4c717697369676e61747572655830b60fe2f649220c17291b1040b988e937dcbc226eb861b5e6c8af53f12ab7ccbf1eb464fa035ab1f559cbab54b9391403")), - tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830183018301830182045820a23c34db2a7e6ae38b72fd5e8c0ce3c9e9c55a82dd03008a96b04f89cf01c02583018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303030318203582048d849ead5f77e5c7323b364d55e0dbdc1e9accb8910bd60b4733905ff2018338302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303282035820c1f1b8e09581d05100c4cd158d9a2f1f07904ecf985cbddd8f4b6c8c2fc968f183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303382035820a0daf418df36efdfb64c1e194da93e030c77456f6f1b69c52fba411c58da3c4e83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303482035820db9b468c37073e3f9101e3237a8a213827d8ff7bfb056e8553bf978b37426e1983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303582035820dd7d18737c30b9868f5fdb9010936fa575aae232117cef355ec4b7a0ea885dd48302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303682035820b967f92823a66f93d9413dc0630ef105e55bb9bd8cb6715af48ed8072e2cea9683018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303037820358201e75ebd88b65febaab87aa83cf7e5a1ea3871a6251a99a701a4300d3dcfd1905830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303882035820df0a11a15db0f1ae9f558f4e10b6fbf3e0eec5f64cec76470ffe0b5552bec1ca83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303982035820c5271184168431ee4aa0b07eb7b08eddf5408f42f95c45a7ee43d99206c11d4c8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313082035820c3262d1345982bf5d1b81238478da309daeedb885db067dad8f590524cb76c2983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313182035820a4494f04372fa36d3f9a9c539b307ef5d34ddb13d8251c6a42784e7dd481a7f783018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313282035820bdaeb12c5e3311f47d898f7a858a53fccd06bb8a73e1199ff1faf3c6f531613083018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031338203582080ad9191c56403bc7aa4d89d5abd1552f706c21ffd2c5a6c7df4ea08838d3ff58302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313482035820ad04ae157fd4864c00bca304b7368e8c162892a9bb3a4a6c500d09f9dfb7cf3983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303135820358206428f5fb201ee3e88e0bfad66733148ddc788652e7601282b2dd3d71d0be7e62830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031368203582050bcf5bf7c581e6f6e07daeb30f032195bdfc192425e39144693224d28dc365383018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313782035820fc3023b7c43895176e869be11522a64d3cfa0a2e3beb6f3c2b384026239e182f8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313882035820759586bfdaeb471b3067d7ab99aff28f0bf7ba4da3e4119bb9693c1b706f133883018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313982035820691125236bc48cc28f38c24cdc96d23b94e8d812126954bdb783aa26e14bd69f83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030323082035820cd2560251877132b8f4b762cf4d273c1588937dea37c66f2a757b67d2a19cd25820458204a29b09fa41ca5cb1e7e796947a7a401f549fa068e7f66b067afddb1698840b5")), + content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581db5d60075aa2a65184b85d6f66c19a53c4ed80273c484222a52e80e30026c636c69656e745f6e6f6e63651b055af797bb85950e6c73657175656e63655f6e756d026974696d657374616d701b179f3ac120a7f6ee7269735f736572766963655f6d657373616765f567636f6e74656e74586a4449444c066b04fdbd95cc0101bfd397b409038ff2d1ef0a049eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a786c01c49ff4e40f056b04cdc2dabb047fc9c68ea0057fd7aba29c0a7f999dafab0d7f0100030000000000000000")), + cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a800000000010000001018301830183024e6365727469666965645f64617461820358204f59e1d67de6bf162f495c7fa46d9693a0e97c1abe2bf6ae2235ae140de512b08204582073cfb1430ef73ab885c27fd576cd8baba82d936234c18a872e75a690c6369be782045820033ceb4a3ee94c0447ef8c00e1901f5c168c6230684f759c98a13dc47a921bed8204582085e48bc6d000256b6d92382c12e88b3143418269dce4335a3a7c93041eca86d78204582052f80434a99bb5174db9bca9f42b4301020993be9be4319ff1fc851e47d8920082045820c63979474a02d2e05c5007ca926d4bdc78c063a648f6295935cbb4154f29405883018204582001d3674a38c0d5b76f56c1d3e99248e716f644422a2c2fa887f80e3600853ee683024474696d65820349eeed9f8592d8cecf17697369676e61747572655830a01893ac403703ebc031837a8a658594abe7aa60fbed3f80343fc992a022c249749851b4a3a7188522f9bd5fd556048c")), + tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830182045820c29cab69304c973adf7b11175fe5a543c0b84f05e92c38d0d3ef6c85150cdcea83025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f3030303030303030303030303030303030303031820358206998260b6d59c04f5035816e29c750d003df78a75b24928d3a6f13a7f78559ee")), }; +// a valid application message +// sequence_num: 3 export const VALID_MESSAGE_SEQ_NUM_3: ClientIncomingMessage = { key: `${GATEWAY_PRINCIPAL}_00000000000000000002`, - content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651bf39dfc3f05b6b1a86c73657175656e63655f6e756d036974696d657374616d701b178e91934f3b735b7269735f736572766963655f6d657373616765f467636f6e74656e74554449444c016c01ad99e7e704710100057465737431")), - cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f646174618203582082e4f0e1f3117e09785f1b5568f0bd7f704a6ea0bd7d79757092990747c4595e82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820bf11fc78a00eb40fcceb76e1b4c55f056c4fe6eaf7af4b4e7525e0c489f61a4882045820aca43161972742f98498c19102837f47510e88151eb257f70ca7515d7b6d255e830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d658203499bc5e7a4b7b2a4c717697369676e61747572655830b60fe2f649220c17291b1040b988e937dcbc226eb861b5e6c8af53f12ab7ccbf1eb464fa035ab1f559cbab54b9391403")), - tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830183018301830182045820a23c34db2a7e6ae38b72fd5e8c0ce3c9e9c55a82dd03008a96b04f89cf01c02583018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303030318203582048d849ead5f77e5c7323b364d55e0dbdc1e9accb8910bd60b4733905ff2018338302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303282035820c1f1b8e09581d05100c4cd158d9a2f1f07904ecf985cbddd8f4b6c8c2fc968f183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303382035820a0daf418df36efdfb64c1e194da93e030c77456f6f1b69c52fba411c58da3c4e83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303482035820db9b468c37073e3f9101e3237a8a213827d8ff7bfb056e8553bf978b37426e1983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303582035820dd7d18737c30b9868f5fdb9010936fa575aae232117cef355ec4b7a0ea885dd48302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303682035820b967f92823a66f93d9413dc0630ef105e55bb9bd8cb6715af48ed8072e2cea9683018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303037820358201e75ebd88b65febaab87aa83cf7e5a1ea3871a6251a99a701a4300d3dcfd1905830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303882035820df0a11a15db0f1ae9f558f4e10b6fbf3e0eec5f64cec76470ffe0b5552bec1ca83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303982035820c5271184168431ee4aa0b07eb7b08eddf5408f42f95c45a7ee43d99206c11d4c8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313082035820c3262d1345982bf5d1b81238478da309daeedb885db067dad8f590524cb76c2983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313182035820a4494f04372fa36d3f9a9c539b307ef5d34ddb13d8251c6a42784e7dd481a7f783018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313282035820bdaeb12c5e3311f47d898f7a858a53fccd06bb8a73e1199ff1faf3c6f531613083018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031338203582080ad9191c56403bc7aa4d89d5abd1552f706c21ffd2c5a6c7df4ea08838d3ff58302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313482035820ad04ae157fd4864c00bca304b7368e8c162892a9bb3a4a6c500d09f9dfb7cf3983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303135820358206428f5fb201ee3e88e0bfad66733148ddc788652e7601282b2dd3d71d0be7e62830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031368203582050bcf5bf7c581e6f6e07daeb30f032195bdfc192425e39144693224d28dc365383018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313782035820fc3023b7c43895176e869be11522a64d3cfa0a2e3beb6f3c2b384026239e182f8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313882035820759586bfdaeb471b3067d7ab99aff28f0bf7ba4da3e4119bb9693c1b706f133883018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313982035820691125236bc48cc28f38c24cdc96d23b94e8d812126954bdb783aa26e14bd69f83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030323082035820cd2560251877132b8f4b762cf4d273c1588937dea37c66f2a757b67d2a19cd25820458204a29b09fa41ca5cb1e7e796947a7a401f549fa068e7f66b067afddb1698840b5")), + content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581db5d60075aa2a65184b85d6f66c19a53c4ed80273c484222a52e80e30026c636c69656e745f6e6f6e63651b055af797bb85950e6c73657175656e63655f6e756d036974696d657374616d701b179f3ac8f4f3cf917269735f736572766963655f6d657373616765f467636f6e74656e74554449444c016c01ad99e7e7047101000548656c6c6f")), + cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a800000000010000001018301830183024e6365727469666965645f6461746182035820730723d4a52e6bde422744cacea54642a6cf82d7df27a6814de5158d125a130b8204582073cfb1430ef73ab885c27fd576cd8baba82d936234c18a872e75a690c6369be782045820033ceb4a3ee94c0447ef8c00e1901f5c168c6230684f759c98a13dc47a921bed8204582085e48bc6d000256b6d92382c12e88b3143418269dce4335a3a7c93041eca86d782045820ec35ff0ab2f8356b168b052bb5a5cb43d56167ab8d49277fdf7e668dd9e8dcbd82045820e74d26118b46be33a9651c0af2357fe1afd13b75d924e5141720597e2056bfaa8301820458208fc214c3a2869a29b8954f67a0d6c0d3f625a11748d496ac631c200001e4e32c83024474696d65820349919fcfa78fd9cecf17697369676e617475726558308c30f497405403b902ecc98d975e8d838a4751c42c07b352c72ec9d619764efb83bb4dde42e05109227ebf31f58a4147")), + tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830182045820ede4ef5ed7dea5775bbf6d451886cd6ce19648b66cc110546db1662ab24d6693830183025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f3030303030303030303030303030303030303032820358206328ef4a59e2f3f5a741f74d3b2fd7c18957237c61dc89a0d43a8d88b37dbc1083025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f3030303030303030303030303030303030303033820358205313f732b30851ef3e0278575f9c959cada96b3de9cdd2d42393d0351eab1707")), }; +// same as VALID_ACK_MESSAGE but with a wrong key export const INVALID_MESSAGE_KEY: ClientIncomingMessage = { key: "wrong-key", // this key is not contained in the tree - content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651b500eeedc6a0443246c73657175656e63655f6e756d016974696d657374616d701b178e8b8e33b7a68b7269735f736572766963655f6d657373616765f567636f6e74656e7458614449444c046b03fdbd95cc0101bfd397b409039eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a78010000011d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e022443046adcee0e50")), - cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f6461746182035820837cecb72b1d6f835b4afebdfa25c0f9363fc60ef76a8f1ff8faab3643a85d4a82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820c57ba057d4a62e4c0c87281acc681df06dd4e662ad70659f2034b6fd84ac385882045820f50b1aca22549e65eb4bc3a8cab4863e1ee207542d1bcb63eeb2756b7cd4230a830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d65820349e9b0edc7e5f1a2c717697369676e61747572655830aa7b6219625b204b5f5f9d1642128799cd3cca476e4dcb39594f7f88df51233e6576ba4404914da8e33dc018c0b374ea")), - tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b65748302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303082035820049a01f5aff5584d8350c83c793a1e645b55ba595b4aceb307266823db0525fa")), + content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581db5d60075aa2a65184b85d6f66c19a53c4ed80273c484222a52e80e30026c636c69656e745f6e6f6e63651b055af797bb85950e6c73657175656e63655f6e756d026974696d657374616d701b179f3ac120a7f6ee7269735f736572766963655f6d657373616765f567636f6e74656e74586a4449444c066b04fdbd95cc0101bfd397b409038ff2d1ef0a049eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a786c01c49ff4e40f056b04cdc2dabb047fc9c68ea0057fd7aba29c0a7f999dafab0d7f0100030000000000000000")), + cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a800000000010000001018301830183024e6365727469666965645f64617461820358204f59e1d67de6bf162f495c7fa46d9693a0e97c1abe2bf6ae2235ae140de512b08204582073cfb1430ef73ab885c27fd576cd8baba82d936234c18a872e75a690c6369be782045820033ceb4a3ee94c0447ef8c00e1901f5c168c6230684f759c98a13dc47a921bed8204582085e48bc6d000256b6d92382c12e88b3143418269dce4335a3a7c93041eca86d78204582052f80434a99bb5174db9bca9f42b4301020993be9be4319ff1fc851e47d8920082045820c63979474a02d2e05c5007ca926d4bdc78c063a648f6295935cbb4154f29405883018204582001d3674a38c0d5b76f56c1d3e99248e716f644422a2c2fa887f80e3600853ee683024474696d65820349eeed9f8592d8cecf17697369676e61747572655830a01893ac403703ebc031837a8a658594abe7aa60fbed3f80343fc992a022c249749851b4a3a7188522f9bd5fd556048c")), + tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830182045820c29cab69304c973adf7b11175fe5a543c0b84f05e92c38d0d3ef6c85150cdcea83025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f3030303030303030303030303030303030303031820358206998260b6d59c04f5035816e29c750d003df78a75b24928d3a6f13a7f78559ee")), }; -export const VALID_ACK_MESSAGE: ClientIncomingMessage = { - key: `${GATEWAY_PRINCIPAL}_00000000000000000001`, - content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651bc3a360e92d91795d6c73657175656e63655f6e756d026974696d657374616d701b178e963bf559dc9c7269735f736572766963655f6d657373616765f567636f6e74656e7458424449444c046b03fdbd95cc0101bfd397b409039eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a780100020100000000000000")), - cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f6461746182035820b5c3eb04d4a7f8cefe27f731536a7bc05d82e47f771e400ee7edcc53be82867982045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820f50d45ee2930b211440bdfbdfce272f673a971ecadf98a54081940ed343fc0b482045820521d9bd182f997f3f2b386a55be42945ff3f79b6e524f0d4d5647a3fc889f49a830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d65820349d683c889d2c7a5c717697369676e61747572655830a468fcc3f684acb3cb9e69ef0be5bf3e5c798aea4ee5bf2caacb96e387167ba83aef4a65f8c5f8268c9ca6c32e8f1965")), - tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b65748301820458203b85c011f8fe15471d1d8204aadab22e9f0292c820dc61a735c20cdee2f8c10a8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303031820358209dda0fc28a093844fcd2475375a182645d7b8d1be6ef84b74ced695f47a06d8a")), +// sequence_num: 4 +export const VALID_CLOSE_MESSAGE: ClientIncomingMessage = { + key: `${GATEWAY_PRINCIPAL}_00000000000000000003`, + content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581db5d60075aa2a65184b85d6f66c19a53c4ed80273c484222a52e80e30026c636c69656e745f6e6f6e63651b055af797bb85950e6c73657175656e63655f6e756d046974696d657374616d701b179f3ac8f4f3cf917269735f736572766963655f6d657373616765f567636f6e74656e7458634449444c066b04fdbd95cc0101bfd397b409038ff2d1ef0a049eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a786c01c49ff4e40f056b04cdc2dabb047fc9c68ea0057fd7aba29c0a7f999dafab0d7f01000200")), + cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a800000000010000001018301830183024e6365727469666965645f6461746182035820730723d4a52e6bde422744cacea54642a6cf82d7df27a6814de5158d125a130b8204582073cfb1430ef73ab885c27fd576cd8baba82d936234c18a872e75a690c6369be782045820033ceb4a3ee94c0447ef8c00e1901f5c168c6230684f759c98a13dc47a921bed8204582085e48bc6d000256b6d92382c12e88b3143418269dce4335a3a7c93041eca86d782045820ec35ff0ab2f8356b168b052bb5a5cb43d56167ab8d49277fdf7e668dd9e8dcbd82045820e74d26118b46be33a9651c0af2357fe1afd13b75d924e5141720597e2056bfaa8301820458208fc214c3a2869a29b8954f67a0d6c0d3f625a11748d496ac631c200001e4e32c83024474696d65820349919fcfa78fd9cecf17697369676e617475726558308c30f497405403b902ecc98d975e8d838a4751c42c07b352c72ec9d619764efb83bb4dde42e05109227ebf31f58a4147")), + tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830182045820ede4ef5ed7dea5775bbf6d451886cd6ce19648b66cc110546db1662ab24d6693830183025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f3030303030303030303030303030303030303032820358206328ef4a59e2f3f5a741f74d3b2fd7c18957237c61dc89a0d43a8d88b37dbc1083025854737164666c2d6d72346b6d2d3268666a792d67616a716f2d78717668372d6866346d662d6e726134692d336974366c2d6e656177342d736f6f6c772d7461655f3030303030303030303030303030303030303033820358205313f732b30851ef3e0278575f9c959cada96b3de9cdd2d42393d0351eab1707")), }; From c086958e87c22d967ce5df06cb9dc8ad64a4bffd Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 15 Dec 2023 09:50:29 +0100 Subject: [PATCH 11/12] perf: rename const https://github.com/omnia-network/ic-websocket-cdk-rs/commit/67b1bdd9e26b748897f8ba1b4a4abe84a28476ea --- src/ic-websocket.test.ts | 14 +++++++------- src/ic-websocket.ts | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index f6cc66d..c07424a 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -5,7 +5,7 @@ import { CallRequest, Cbor } from "@dfinity/agent"; import { IDL } from "@dfinity/candid"; import { Principal } from "@dfinity/principal"; -import IcWebSocket, { MAX_ALLOWED_NETWORK_LATENCY_MS, createWsConfig } from "./ic-websocket"; +import IcWebSocket, { COMMUNICATION_LATENCY_BOUND_MS, createWsConfig } from "./ic-websocket"; import { generateRandomIdentity } from "./identity"; import { CanisterWsMessageArguments, @@ -241,7 +241,7 @@ describe("IcWebsocket class", () => { mockWsServer.send(encodeHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY)); // advance the open timeout - await jest.advanceTimersByTimeAsync(2 * MAX_ALLOWED_NETWORK_LATENCY_MS); + await jest.advanceTimersByTimeAsync(2 * COMMUNICATION_LATENCY_BOUND_MS); expect(icWs["_isConnectionEstablished"]).toEqual(false); expect(onOpen).not.toHaveBeenCalled(); @@ -313,7 +313,7 @@ describe("IcWebsocket class", () => { await sleep(100); // wait for the open timeout so that it expires - await sleep(2 * MAX_ALLOWED_NETWORK_LATENCY_MS); + await sleep(2 * COMMUNICATION_LATENCY_BOUND_MS); expect(onOpen).toHaveBeenCalled(); expect(icWs["_isConnectionEstablished"]).toEqual(true); @@ -321,7 +321,7 @@ describe("IcWebsocket class", () => { expect(icWs.readyState).toEqual(WebSocket.OPEN); // make sure onmessage callback is not called when receiving the first message expect(onMessage).not.toHaveBeenCalled(); - }, 3 * MAX_ALLOWED_NETWORK_LATENCY_MS); + }, 3 * COMMUNICATION_LATENCY_BOUND_MS); it("onmessage is called when a valid message is received", async () => { const onMessage = jest.fn(); @@ -563,7 +563,7 @@ describe("Messages acknowledgement", () => { await mockWsServer.nextMessage; // make the ack timeout expire - await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + MAX_ALLOWED_NETWORK_LATENCY_MS); + await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + COMMUNICATION_LATENCY_BOUND_MS); const ackTimeoutError = new Error(`Ack message timeout. Not received ack for sequence numbers: ${[BigInt(1)]}`); expect(onError).toHaveBeenCalledWith(new ErrorEvent("error", { error: ackTimeoutError })); @@ -610,7 +610,7 @@ describe("Messages acknowledgement", () => { await mockWsServer.nextMessage; // make the ack timeout expire - await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + MAX_ALLOWED_NETWORK_LATENCY_MS); + await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + COMMUNICATION_LATENCY_BOUND_MS); // send the ack message from the canister // when the ack timeout is already expired @@ -667,7 +667,7 @@ describe("Messages acknowledgement", () => { console.log("sent ack message from canister"); // make the ack timeout expire - await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + MAX_ALLOWED_NETWORK_LATENCY_MS); + await jest.advanceTimersByTimeAsync(ackMessageIntervalMs + COMMUNICATION_LATENCY_BOUND_MS); // first message has been acknowledged correctly, // as the error only reports the missing ack for the keep alive response diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index e3347fd..9d413b2 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -41,11 +41,11 @@ import { WsAgent } from "./agent"; */ const DEFAULT_ACK_MESSAGE_INTERVAL_MS = 300_000; /** - * The maximum latency allowed between the client and the canister. + * The maximum communication latency allowed between the client and the canister (same as in the canister). * * Used to determine the ack message timeout. */ -export const MAX_ALLOWED_NETWORK_LATENCY_MS = 30_000; +export const COMMUNICATION_LATENCY_BOUND_MS = 30_000; /** * Interface to create a new IcWebSocketConfig. For a simple configuration, use {@link createWsConfig}. @@ -181,7 +181,7 @@ export default class IcWebSocket< }); this._ackMessagesQueue = new AckMessagesQueue({ - expirationMs: (config.ackMessageIntervalMs || DEFAULT_ACK_MESSAGE_INTERVAL_MS) + MAX_ALLOWED_NETWORK_LATENCY_MS, + expirationMs: (config.ackMessageIntervalMs || DEFAULT_ACK_MESSAGE_INTERVAL_MS) + COMMUNICATION_LATENCY_BOUND_MS, timeoutExpiredCallback: this._onAckMessageTimeout.bind(this), }); @@ -244,7 +244,7 @@ export default class IcWebSocket< } this._openTimeout = null; - }, 2 * MAX_ALLOWED_NETWORK_LATENCY_MS); + }, 2 * COMMUNICATION_LATENCY_BOUND_MS); } private _cancelOpenTimeout() { From eac35281f163efdcdd6380b0a10f4ba85054ffb5 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 15 Dec 2023 22:23:47 +0100 Subject: [PATCH 12/12] chore: bump to version v0.3.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 894c104..9e46ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ic-websocket-js", - "version": "0.3.1", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ic-websocket-js", - "version": "0.3.1", + "version": "0.3.2", "license": "MIT", "dependencies": { "loglevel": "^1.8.1" diff --git a/package.json b/package.json index f0266c6..dad52f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ic-websocket-js", - "version": "0.3.1", + "version": "0.3.2", "description": "IC WebSocket on the Internet Computer", "license": "MIT", "repository": {