From e9e26284b38949c4ff8e8390ea1d8f2a1e689a7b Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Wed, 15 Nov 2023 16:17:47 +0100 Subject: [PATCH 1/8] feat: send gateway principal in the open message --- src/ic-websocket.test.ts | 43 +++++++++++++++++++++++++++++++++++ src/ic-websocket.ts | 11 +++++++-- src/idl.ts | 28 ++++++++++++++--------- src/test/actor.ts | 49 ++++++++-------------------------------- src/test/constants.ts | 3 +++ src/test/messages.ts | 9 ++++---- 6 files changed, 87 insertions(+), 56 deletions(-) create mode 100644 src/test/constants.ts diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 369b142..365ec0c 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -13,6 +13,7 @@ import { INVALID_MESSAGE_KEY, VALID_ACK_MESSAGE, VALID_MESSAGE_SEQ_NUM_2, VALID_ 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"; const wsGatewayAddress = "ws://127.0.0.1:8080"; // the canister from which the application message was sent (needed to verify the message certificate) @@ -22,6 +23,7 @@ const testCanisterActor = getTestCanisterActor(canisterId); const icWebsocketConfig = createWsConfig({ canisterId: canisterId.toText(), + gatewayPrincipal: GATEWAY_PRINCIPAL.toText(), canisterActor: testCanisterActor, networkUrl: icNetworkUrl, identity: generateRandomIdentity(), @@ -65,6 +67,46 @@ describe("IcWebsocket class", () => { expect(onError).toHaveBeenCalled(); }); + it("throws an error if the canisterId is not provided or invalid", () => { + let icWsConfig = createWsConfig({ ...icWebsocketConfig }); + // @ts-ignore + delete icWsConfig.canisterId; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); + + icWsConfig = createWsConfig({ ...icWebsocketConfig }); + icWsConfig.canisterId = "invalid"; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); + }); + + it("throws an error if the gatewayPrincipal is not provided or invalid", () => { + let icWsConfig = createWsConfig({ ...icWebsocketConfig }); + // @ts-ignore + delete icWsConfig.gatewayPrincipal; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); + + icWsConfig = createWsConfig({ ...icWebsocketConfig }); + icWsConfig.gatewayPrincipal = "invalid"; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); + }); + + it("passes if the canisterId is a valid string or principal", () => { + let icWsConfig = createWsConfig({ ...icWebsocketConfig }); + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); + + icWsConfig = createWsConfig({ ...icWebsocketConfig }); + icWsConfig.canisterId = canisterId; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); + }); + + it("passes if the gatewayPrincipal is a valid string or principal", () => { + let icWsConfig = createWsConfig({ ...icWebsocketConfig }); + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); + + icWsConfig = createWsConfig({ ...icWebsocketConfig }); + icWsConfig.gatewayPrincipal = GATEWAY_PRINCIPAL; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); + }); + it("throws an error if the canisterActor is not provided", () => { const icWsConfig = createWsConfig({ ...icWebsocketConfig }); // @ts-ignore @@ -132,6 +174,7 @@ describe("IcWebsocket class", () => { expect(openMessageContent.method_name).toEqual("ws_open"); expect(IDL.decode(wsOpenIdl.argTypes, openMessageContent.arg)[0]).toMatchObject({ client_nonce: clientKey.client_nonce, + gateway_principal: GATEWAY_PRINCIPAL, }); }); diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index 311f25a..5b4f997 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -46,7 +46,11 @@ export interface IcWebSocketConfig { /** * The canister id of the canister to open the WebSocket to. */ - canisterId: string; + canisterId: string | Principal; + /** + * The principal of the gateway to which the WebSocket will be opened. + */ + gatewayPrincipal: string | Principal; /** * The canister actor used to serialize and deserialize the application messages. */ @@ -86,6 +90,7 @@ export default class IcWebSocket< ApplicationMessageType = GetApplicationMessageType > { public readonly canisterId: Principal; + public readonly gatewayPrincipal: Principal; private readonly _canisterActor: ActorSubclass; private readonly _applicationMessageIdl: IDL.Type; private readonly _httpAgent: HttpAgent; @@ -126,7 +131,8 @@ export default class IcWebSocket< * @param config The IcWebSocket configuration. Use {@link createWsConfig} to create a new configuration. */ constructor(url: WsParameters[0], protocols: WsParameters[1], config: IcWebSocketConfig) { - this.canisterId = Principal.fromText(config.canisterId); + this.canisterId = Principal.from(config.canisterId); + this.gatewayPrincipal = Principal.from(config.gatewayPrincipal); if (!config.canisterActor) { throw new Error("Canister actor is required"); @@ -228,6 +234,7 @@ export default class IcWebSocket< this._wsAgent, { client_nonce: this._clientKey.client_nonce, + gateway_principal: this.gatewayPrincipal, } ); diff --git a/src/idl.ts b/src/idl.ts index dcc9bd3..ba0ac5a 100644 --- a/src/idl.ts +++ b/src/idl.ts @@ -3,7 +3,8 @@ import type { Principal } from '@dfinity/principal'; import { Actor, ActorSubclass, type ActorMethod } from '@dfinity/agent'; import type { GetInnerType } from "./types"; -export type ClientPrincipal = Principal; +type ClientPrincipal = Principal; +type GatewayPrincipal = Principal; export type ClientKey = { 'client_principal': ClientPrincipal, 'client_nonce': bigint, @@ -20,6 +21,7 @@ export type CanisterWsMessageResult = { 'Ok': null } | { 'Err': string }; export type CanisterWsOpenArguments = { 'client_nonce': bigint, + 'gateway_principal': GatewayPrincipal, }; export type CanisterWsOpenResult = { 'Ok': null } | { 'Err': string }; @@ -36,8 +38,9 @@ export interface _WS_CANISTER_SERVICE { */ export type GetApplicationMessageType = Exclude[1], []>[0]; -export const ClientPrincipalIdl = IDL.Principal; -export const ClientKeyIdl = IDL.Record({ +const ClientPrincipalIdl = IDL.Principal; +const GatewayPrincipalIdl = IDL.Principal; +const ClientKeyIdl = IDL.Record({ 'client_principal': ClientPrincipalIdl, 'client_nonce': IDL.Nat64, }); @@ -49,12 +52,15 @@ const WebsocketMessageIdl = IDL.Record({ 'timestamp': IDL.Nat64, 'is_service_message': IDL.Bool, }); -const CanisterWsMessageArgumentsIdl = IDL.Record({ 'msg': WebsocketMessageIdl }); -const CanisterWsMessageResultIdl = IDL.Variant({ +export const CanisterWsMessageArgumentsIdl = IDL.Record({ 'msg': WebsocketMessageIdl }); +export const CanisterWsMessageResultIdl = IDL.Variant({ 'Ok': IDL.Null, 'Err': IDL.Text, }); -const CanisterWsOpenArgumentsIdl = IDL.Record({ 'client_nonce': IDL.Nat64 }); +const CanisterWsOpenArgumentsIdl = IDL.Record({ + 'client_nonce': IDL.Nat64, + 'gateway_principal': GatewayPrincipalIdl, +}); const CanisterWsOpenResultIdl = IDL.Variant({ 'Ok': IDL.Null, 'Err': IDL.Text, @@ -63,7 +69,7 @@ const CanisterWsOpenResultIdl = IDL.Variant({ export const wsOpenIdl = IDL.Func([CanisterWsOpenArgumentsIdl], [CanisterWsOpenResultIdl], []); export const wsMessageIdl = IDL.Func([CanisterWsMessageArgumentsIdl, IDL.Opt(IDL.Null)], [CanisterWsMessageResultIdl], []); -export type CanisterOpenMessageContent = { +type CanisterOpenMessageContent = { 'client_key': ClientKey, }; export type CanisterAckMessageContent = { @@ -80,16 +86,16 @@ export type WebsocketServiceMessageContent = { KeepAliveMessage: ClientKeepAliveMessageContent, }; -export const CanisterOpenMessageContentIdl = IDL.Record({ +const CanisterOpenMessageContentIdl = IDL.Record({ 'client_key': ClientKeyIdl, }); -export const CanisterAckMessageContentIdl = IDL.Record({ +const CanisterAckMessageContentIdl = IDL.Record({ 'last_incoming_sequence_num': IDL.Nat64, }); -export const ClientKeepAliveMessageContentIdl = IDL.Record({ +const ClientKeepAliveMessageContentIdl = IDL.Record({ 'last_incoming_sequence_num': IDL.Nat64, }); -export const WebsocketServiceMessageContentIdl = IDL.Variant({ +const WebsocketServiceMessageContentIdl = IDL.Variant({ 'OpenMessage': CanisterOpenMessageContentIdl, 'AckMessage': CanisterAckMessageContentIdl, 'KeepAliveMessage': ClientKeepAliveMessageContentIdl, diff --git a/src/test/actor.ts b/src/test/actor.ts index 8565fcb..c9d1658 100644 --- a/src/test/actor.ts +++ b/src/test/actor.ts @@ -1,56 +1,27 @@ import { Actor, ActorMethod, ActorSubclass } from "@dfinity/agent"; import { IDL } from "@dfinity/candid"; import { Principal } from "@dfinity/principal"; +import { + CanisterWsMessageArguments, + CanisterWsMessageArgumentsIdl, + CanisterWsMessageResult, + CanisterWsMessageResultIdl, + CanisterWsOpenArguments, + CanisterWsOpenResult, +} from "../idl"; const testCanisterIdlFactory: IDL.InterfaceFactory = ({ IDL }) => { - const ClientPrincipal = IDL.Principal; - const ClientKey = IDL.Record({ - 'client_principal': ClientPrincipal, - 'client_nonce': IDL.Nat64, - }); - const WebsocketMessage = IDL.Record({ - 'sequence_num': IDL.Nat64, - 'content': IDL.Vec(IDL.Nat8), - 'client_key': ClientKey, - 'timestamp': IDL.Nat64, - 'is_service_message': IDL.Bool, - }); - const CanisterWsMessageArguments = IDL.Record({ 'msg': WebsocketMessage }); const AppMessage = IDL.Record({ 'text': IDL.Text }); - const CanisterWsMessageResult = IDL.Variant({ - 'Ok': IDL.Null, - 'Err': IDL.Text, - }); return IDL.Service({ 'ws_message': IDL.Func( - [CanisterWsMessageArguments, IDL.Opt(AppMessage)], - [CanisterWsMessageResult], + [CanisterWsMessageArgumentsIdl, IDL.Opt(AppMessage)], + [CanisterWsMessageResultIdl], [], ), }); } export type AppMessage = { 'text' : string }; -export interface CanisterWsMessageArguments { 'msg' : WebsocketMessage } -export type CanisterWsMessageResult = { 'Ok' : null } | - { 'Err' : string }; -export interface ClientKey { - 'client_principal' : ClientPrincipal, - 'client_nonce' : bigint, -} -export type ClientPrincipal = Principal; -export interface WebsocketMessage { - 'sequence_num' : bigint, - 'content' : Uint8Array | number[], - 'client_key' : ClientKey, - 'timestamp' : bigint, - 'is_service_message' : boolean, -} -export type CanisterWsOpenArguments = { - 'client_nonce': bigint, -}; -export type CanisterWsOpenResult = { 'Ok': null } | -{ 'Err': string }; export interface _SERVICE { 'ws_message' : ActorMethod< [CanisterWsMessageArguments, [] | [AppMessage]], diff --git a/src/test/constants.ts b/src/test/constants.ts new file mode 100644 index 0000000..828e636 --- /dev/null +++ b/src/test/constants.ts @@ -0,0 +1,3 @@ +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 diff --git a/src/test/messages.ts b/src/test/messages.ts index e99622d..f84991d 100644 --- a/src/test/messages.ts +++ b/src/test/messages.ts @@ -1,24 +1,25 @@ import { fromHex } from "@dfinity/agent"; import { ClientIncomingMessage } from "../types"; +import { GATEWAY_PRINCIPAL } from "./constants"; // Messages generated from a canister running in a local replica export const VALID_OPEN_MESSAGE: ClientIncomingMessage = { - key: "i3gux-m3hwt-5mh2w-t7wwm-fwx5j-6z6ht-hxguo-t4rfw-qp24z-g5ivt-2qe_00000000000000000000", + key: `${GATEWAY_PRINCIPAL}_00000000000000000000`, content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651b500eeedc6a0443246c73657175656e63655f6e756d016974696d657374616d701b178e8b8e33b7a68b7269735f736572766963655f6d657373616765f567636f6e74656e7458614449444c046b03fdbd95cc0101bfd397b409039eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a78010000011d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e022443046adcee0e50")), cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f6461746182035820837cecb72b1d6f835b4afebdfa25c0f9363fc60ef76a8f1ff8faab3643a85d4a82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820c57ba057d4a62e4c0c87281acc681df06dd4e662ad70659f2034b6fd84ac385882045820f50b1aca22549e65eb4bc3a8cab4863e1ee207542d1bcb63eeb2756b7cd4230a830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d65820349e9b0edc7e5f1a2c717697369676e61747572655830aa7b6219625b204b5f5f9d1642128799cd3cca476e4dcb39594f7f88df51233e6576ba4404914da8e33dc018c0b374ea")), tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b65748302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303082035820049a01f5aff5584d8350c83c793a1e645b55ba595b4aceb307266823db0525fa")), }; export const VALID_MESSAGE_SEQ_NUM_2: ClientIncomingMessage = { - key: "i3gux-m3hwt-5mh2w-t7wwm-fwx5j-6z6ht-hxguo-t4rfw-qp24z-g5ivt-2qe_00000000000000000001", + key: `${GATEWAY_PRINCIPAL}_00000000000000000001`, content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651bf39dfc3f05b6b1a86c73657175656e63655f6e756d026974696d657374616d701b178e91934f3b735b7269735f736572766963655f6d657373616765f467636f6e74656e74554449444c016c01ad99e7e704710100057465737430")), cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f646174618203582082e4f0e1f3117e09785f1b5568f0bd7f704a6ea0bd7d79757092990747c4595e82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820bf11fc78a00eb40fcceb76e1b4c55f056c4fe6eaf7af4b4e7525e0c489f61a4882045820aca43161972742f98498c19102837f47510e88151eb257f70ca7515d7b6d255e830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d658203499bc5e7a4b7b2a4c717697369676e61747572655830b60fe2f649220c17291b1040b988e937dcbc226eb861b5e6c8af53f12ab7ccbf1eb464fa035ab1f559cbab54b9391403")), tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830183018301830182045820a23c34db2a7e6ae38b72fd5e8c0ce3c9e9c55a82dd03008a96b04f89cf01c02583018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303030318203582048d849ead5f77e5c7323b364d55e0dbdc1e9accb8910bd60b4733905ff2018338302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303282035820c1f1b8e09581d05100c4cd158d9a2f1f07904ecf985cbddd8f4b6c8c2fc968f183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303382035820a0daf418df36efdfb64c1e194da93e030c77456f6f1b69c52fba411c58da3c4e83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303482035820db9b468c37073e3f9101e3237a8a213827d8ff7bfb056e8553bf978b37426e1983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303582035820dd7d18737c30b9868f5fdb9010936fa575aae232117cef355ec4b7a0ea885dd48302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303682035820b967f92823a66f93d9413dc0630ef105e55bb9bd8cb6715af48ed8072e2cea9683018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303037820358201e75ebd88b65febaab87aa83cf7e5a1ea3871a6251a99a701a4300d3dcfd1905830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303882035820df0a11a15db0f1ae9f558f4e10b6fbf3e0eec5f64cec76470ffe0b5552bec1ca83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303982035820c5271184168431ee4aa0b07eb7b08eddf5408f42f95c45a7ee43d99206c11d4c8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313082035820c3262d1345982bf5d1b81238478da309daeedb885db067dad8f590524cb76c2983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313182035820a4494f04372fa36d3f9a9c539b307ef5d34ddb13d8251c6a42784e7dd481a7f783018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313282035820bdaeb12c5e3311f47d898f7a858a53fccd06bb8a73e1199ff1faf3c6f531613083018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031338203582080ad9191c56403bc7aa4d89d5abd1552f706c21ffd2c5a6c7df4ea08838d3ff58302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313482035820ad04ae157fd4864c00bca304b7368e8c162892a9bb3a4a6c500d09f9dfb7cf3983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303135820358206428f5fb201ee3e88e0bfad66733148ddc788652e7601282b2dd3d71d0be7e62830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031368203582050bcf5bf7c581e6f6e07daeb30f032195bdfc192425e39144693224d28dc365383018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313782035820fc3023b7c43895176e869be11522a64d3cfa0a2e3beb6f3c2b384026239e182f8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313882035820759586bfdaeb471b3067d7ab99aff28f0bf7ba4da3e4119bb9693c1b706f133883018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313982035820691125236bc48cc28f38c24cdc96d23b94e8d812126954bdb783aa26e14bd69f83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030323082035820cd2560251877132b8f4b762cf4d273c1588937dea37c66f2a757b67d2a19cd25820458204a29b09fa41ca5cb1e7e796947a7a401f549fa068e7f66b067afddb1698840b5")), }; export const VALID_MESSAGE_SEQ_NUM_3: ClientIncomingMessage = { - key: "i3gux-m3hwt-5mh2w-t7wwm-fwx5j-6z6ht-hxguo-t4rfw-qp24z-g5ivt-2qe_00000000000000000002", + key: `${GATEWAY_PRINCIPAL}_00000000000000000002`, content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651bf39dfc3f05b6b1a86c73657175656e63655f6e756d036974696d657374616d701b178e91934f3b735b7269735f736572766963655f6d657373616765f467636f6e74656e74554449444c016c01ad99e7e704710100057465737431")), cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f646174618203582082e4f0e1f3117e09785f1b5568f0bd7f704a6ea0bd7d79757092990747c4595e82045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820bf11fc78a00eb40fcceb76e1b4c55f056c4fe6eaf7af4b4e7525e0c489f61a4882045820aca43161972742f98498c19102837f47510e88151eb257f70ca7515d7b6d255e830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d658203499bc5e7a4b7b2a4c717697369676e61747572655830b60fe2f649220c17291b1040b988e937dcbc226eb861b5e6c8af53f12ab7ccbf1eb464fa035ab1f559cbab54b9391403")), tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b6574830183018301830182045820a23c34db2a7e6ae38b72fd5e8c0ce3c9e9c55a82dd03008a96b04f89cf01c02583018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303030318203582048d849ead5f77e5c7323b364d55e0dbdc1e9accb8910bd60b4733905ff2018338302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303282035820c1f1b8e09581d05100c4cd158d9a2f1f07904ecf985cbddd8f4b6c8c2fc968f183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303382035820a0daf418df36efdfb64c1e194da93e030c77456f6f1b69c52fba411c58da3c4e83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303482035820db9b468c37073e3f9101e3237a8a213827d8ff7bfb056e8553bf978b37426e1983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303582035820dd7d18737c30b9868f5fdb9010936fa575aae232117cef355ec4b7a0ea885dd48302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303682035820b967f92823a66f93d9413dc0630ef105e55bb9bd8cb6715af48ed8072e2cea9683018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303037820358201e75ebd88b65febaab87aa83cf7e5a1ea3871a6251a99a701a4300d3dcfd1905830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303882035820df0a11a15db0f1ae9f558f4e10b6fbf3e0eec5f64cec76470ffe0b5552bec1ca83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030303982035820c5271184168431ee4aa0b07eb7b08eddf5408f42f95c45a7ee43d99206c11d4c8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313082035820c3262d1345982bf5d1b81238478da309daeedb885db067dad8f590524cb76c2983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313182035820a4494f04372fa36d3f9a9c539b307ef5d34ddb13d8251c6a42784e7dd481a7f783018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313282035820bdaeb12c5e3311f47d898f7a858a53fccd06bb8a73e1199ff1faf3c6f531613083018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031338203582080ad9191c56403bc7aa4d89d5abd1552f706c21ffd2c5a6c7df4ea08838d3ff58302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313482035820ad04ae157fd4864c00bca304b7368e8c162892a9bb3a4a6c500d09f9dfb7cf3983018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303135820358206428f5fb201ee3e88e0bfad66733148ddc788652e7601282b2dd3d71d0be7e62830183018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f30303030303030303030303030303030303031368203582050bcf5bf7c581e6f6e07daeb30f032195bdfc192425e39144693224d28dc365383018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313782035820fc3023b7c43895176e869be11522a64d3cfa0a2e3beb6f3c2b384026239e182f8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313882035820759586bfdaeb471b3067d7ab99aff28f0bf7ba4da3e4119bb9693c1b706f133883018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030313982035820691125236bc48cc28f38c24cdc96d23b94e8d812126954bdb783aa26e14bd69f83018302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f303030303030303030303030303030303030323082035820cd2560251877132b8f4b762cf4d273c1588937dea37c66f2a757b67d2a19cd25820458204a29b09fa41ca5cb1e7e796947a7a401f549fa068e7f66b067afddb1698840b5")), @@ -32,7 +33,7 @@ export const INVALID_MESSAGE_KEY: ClientIncomingMessage = { }; export const VALID_ACK_MESSAGE: ClientIncomingMessage = { - key: "i3gux-m3hwt-5mh2w-t7wwm-fwx5j-6z6ht-hxguo-t4rfw-qp24z-g5ivt-2qe_00000000000000000001", + key: `${GATEWAY_PRINCIPAL}_00000000000000000001`, content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651bc3a360e92d91795d6c73657175656e63655f6e756d026974696d657374616d701b178e963bf559dc9c7269735f736572766963655f6d657373616765f567636f6e74656e7458424449444c046b03fdbd95cc0101bfd397b409039eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a780100020100000000000000")), cert: new Uint8Array(fromHex("d9d9f7a2647472656583018301830183024863616e6973746572830183024a8000000000100000010183018301830183024e6365727469666965645f6461746182035820b5c3eb04d4a7f8cefe27f731536a7bc05d82e47f771e400ee7edcc53be82867982045820f8d20e36feb79f8495eb4c632b7a04171599957c8c267f55b2156d89b5c1e42482045820b303c22a513bc816b29dcba66f3b0e77343b4c56a5d8a12c8bda52cc6dd580e482045820eb5623c70e5668b5ad90b62f0dcb893824ff63d09552c40e6ecdfbc51f5534248204582077d28a3053cd3845a065a879ce36849add41cae9e6a56d452e328194b38e15ec82045820f50d45ee2930b211440bdfbdfce272f673a971ecadf98a54081940ed343fc0b482045820521d9bd182f997f3f2b386a55be42945ff3f79b6e524f0d4d5647a3fc889f49a830182045820d6f983879ac42c7f002d19fc541f62ec565ec7dc211e3bb29b2e8f86f83e37c083024474696d65820349d683c889d2c7a5c717697369676e61747572655830a468fcc3f684acb3cb9e69ef0be5bf3e5c798aea4ee5bf2caacb96e387167ba83aef4a65f8c5f8268c9ca6c32e8f1965")), tree: new Uint8Array(fromHex("d9d9f7830249776562736f636b65748301820458203b85c011f8fe15471d1d8204aadab22e9f0292c820dc61a735c20cdee2f8c10a8302585469336775782d6d336877742d356d6832772d743777776d2d667778356a2d367a3668742d687867756f2d74347266772d717032347a2d67356976742d3271655f3030303030303030303030303030303030303031820358209dda0fc28a093844fcd2475375a182645d7b8d1be6ef84b74ced695f47a06d8a")), From ee9dd1be7c45893671b071bf4783c3cfca84124a Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Thu, 16 Nov 2023 08:22:34 +0100 Subject: [PATCH 2/8] chore: enable tests on pull request --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 48a1843..0c48406 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,7 @@ on: push: branches: - main + pull_request: jobs: tests: From c92d69f812a530a7c4be53931e2d3fbfba72fccf Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 17 Nov 2023 20:17:22 +0100 Subject: [PATCH 3/8] chore: update @dfinity dependencies to v0.20.0 --- package-lock.json | 169 ++++++++++++++++++++++++---------------------- package.json | 9 ++- src/utils.ts | 2 +- 3 files changed, 93 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 305b36a..9347b18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "process": "0.11.10", "stream-browserify": "3.0.0", "ts-jest": "^29.1.1", - "tweetnacl": "^1.0.3", "typescript": "^5.1.6", "util": "0.12.4" }, @@ -40,10 +39,10 @@ "node": "^14 || ^16 || ^18" }, "peerDependencies": { - "@dfinity/agent": "^0.19.3", - "@dfinity/candid": "^0.19.3", - "@dfinity/identity-secp256k1": "^0.19.3", - "@dfinity/principal": "^0.19.3" + "@dfinity/agent": "^0.20.0", + "@dfinity/candid": "^0.20.0", + "@dfinity/identity-secp256k1": "^0.20.0", + "@dfinity/principal": "^0.20.0" } }, "node_modules/@ampproject/remapping": { @@ -2336,47 +2335,96 @@ } }, "node_modules/@dfinity/agent": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.19.3.tgz", - "integrity": "sha512-q410aNLoOA1ZkwdAMgSo6t++pjISn9TfSybRXhPRI5Ume7eG6+6qYr/rlvcXy7Nb2+Ar7LTsHNn34IULfjni7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.20.0.tgz", + "integrity": "sha512-KXCBGEUxyCtkJ4IkpVo+hKIfjILWKatqbWeTME9VdaTiRp0aEhOsqlMSAih5BC0nfaNqHVmNLo6W3lYVJpL1Mg==", "peer": true, "dependencies": { + "@noble/curves": "^1.2.0", "@noble/hashes": "^1.3.1", "base64-arraybuffer": "^0.2.0", "borc": "^2.1.1", + "buffer": "^6.0.3", "simple-cbor": "^0.4.1" }, "peerDependencies": { - "@dfinity/candid": "^0.19.3", - "@dfinity/principal": "^0.19.3" + "@dfinity/candid": "^0.20.0", + "@dfinity/principal": "^0.20.0" } }, "node_modules/@dfinity/candid": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.19.3.tgz", - "integrity": "sha512-yXfbLSWTeRd4G0bLLxYoDqpXH3Jim0P+1PPZOoktXNC1X1hB+ea3yrZebX75t4GVoQK7123F7mxWHiPjuV2tQQ==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.20.0.tgz", + "integrity": "sha512-GoWmNtop+Jbd3IgZHbyz3BG2CO4yvLrW/1k7+1QMqDMWAYFUe+K87G2jPqH5Fmt3ftkzjzCgFpuH0DqSFCCTPA==", "peer": true, "peerDependencies": { - "@dfinity/principal": "^0.19.3" + "@dfinity/principal": "^0.20.0" } }, "node_modules/@dfinity/identity-secp256k1": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@dfinity/identity-secp256k1/-/identity-secp256k1-0.19.3.tgz", - "integrity": "sha512-mllF0y1p5zmZo24vqCSXTHAT89z4p0e90/23BjMlyWLjVlpZ4YSXThWILKg/z4e0cbiLpadScOPIy5scbaC90g==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@dfinity/identity-secp256k1/-/identity-secp256k1-0.20.0.tgz", + "integrity": "sha512-UCFncyEvCZo/TrgsKNsSKIcx6ZEYY9BbUZ6cvack0TJn6FYdjLoIihbzDlpfMWDM9iJ9J1a56Iqrl7WX4ENPJg==", "peer": true, "dependencies": { - "@dfinity/agent": "^0.19.3", + "@dfinity/agent": "^0.20.0", "@noble/hashes": "^1.3.1", - "bip39": "^3.0.4", - "bs58check": "^2.1.2", - "secp256k1": "^4.0.3" + "bip39": "^3.1.0", + "bs58check": "^3.0.1", + "hdkey": "^2.1.0", + "secp256k1": "^5.0.0" + } + }, + "node_modules/@dfinity/identity-secp256k1/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", + "peer": true + }, + "node_modules/@dfinity/identity-secp256k1/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "peer": true, + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@dfinity/identity-secp256k1/node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "peer": true, + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, + "node_modules/@dfinity/identity-secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "peer": true + }, + "node_modules/@dfinity/identity-secp256k1/node_modules/secp256k1": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", + "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", + "hasInstallScript": true, + "peer": true, + "dependencies": { + "elliptic": "^6.5.4", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/@dfinity/principal": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.19.3.tgz", - "integrity": "sha512-+nixVvdGt7ECxRvLXDXsvU9q9sSPssBtDQ4bXa149SK6gcYcmZ6lfWIi3DJNqj3tGROxILVBsguel9tECappsA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.20.0.tgz", + "integrity": "sha512-g64nQHUP0i/HNqS/iQb+SXstKWy0Mk4b3XhXqoHLTh2+S04M9CCKmEtq7h6ryVfa0lUhvTdaDKnrsmRHzP0czQ==", "peer": true, "dependencies": { "@noble/hashes": "^1.3.1" @@ -4161,11 +4209,22 @@ "semver": "bin/semver.js" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "peer": true, + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "peer": true, "engines": { "node": ">= 16" }, @@ -6178,21 +6237,13 @@ } }, "node_modules/bip39": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", - "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", "dependencies": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" + "@noble/hashes": "^1.2.0" } }, - "node_modules/bip39/node_modules/@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -6469,7 +6520,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -7209,19 +7259,6 @@ "sha.js": "^2.4.0" } }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -9348,7 +9385,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-2.1.0.tgz", "integrity": "sha512-i9Wzi0Dy49bNS4tXXeGeu0vIcn86xXdPQUpEYg+SO1YiO8HtomjmmRMaRyqL0r59QfcD4PfVbSF3qmsWFwAemA==", - "dev": true, "dependencies": { "bs58check": "^2.1.2", "ripemd160": "^2.0.2", @@ -13926,21 +13962,6 @@ "node": ">=8" } }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -14403,14 +14424,6 @@ ], "optional": true }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -16384,12 +16397,6 @@ "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", "dev": true }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "dev": true - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/package.json b/package.json index 57778a7..740fc06 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "process": "0.11.10", "stream-browserify": "3.0.0", "ts-jest": "^29.1.1", - "tweetnacl": "^1.0.3", "typescript": "^5.1.6", "util": "0.12.4" }, @@ -61,9 +60,9 @@ "loglevel": "^1.8.1" }, "peerDependencies": { - "@dfinity/agent": "^0.19.3", - "@dfinity/candid": "^0.19.3", - "@dfinity/identity-secp256k1": "^0.19.3", - "@dfinity/principal": "^0.19.3" + "@dfinity/agent": "^0.20.0", + "@dfinity/candid": "^0.20.0", + "@dfinity/identity-secp256k1": "^0.20.0", + "@dfinity/principal": "^0.20.0" } } diff --git a/src/utils.ts b/src/utils.ts index 26afc6a..f4f5090 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -76,7 +76,7 @@ export const isMessageBodyValid = async ( return false; } - return !!treeSha && areBuffersEqual(sha, treeSha); + return !!treeSha && areBuffersEqual(sha, treeSha as ArrayBuffer); }; export const safeExecute = async ( From 71b94c441812ca8a409ca24b54f41c7263714bd2 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 17 Nov 2023 20:29:37 +0100 Subject: [PATCH 4/8] feat: handshake message from gateway --- src/ic-websocket.test.ts | 79 +++++++++++++++++++++++---------- src/ic-websocket.ts | 96 ++++++++++++++++++++++++++-------------- src/test/messages.ts | 19 ++++++-- src/types.test.ts | 88 ++++++++++++++++++++++++++++++++++++ src/types.ts | 32 +++++++++++++- 5 files changed, 253 insertions(+), 61 deletions(-) create mode 100644 src/types.test.ts diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 365ec0c..894d559 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -7,13 +7,14 @@ import { IDL } from "@dfinity/candid"; import IcWebSocket, { createWsConfig } from "./ic-websocket"; import { Principal } from "@dfinity/principal"; import { generateRandomIdentity } from "./identity"; -import { CanisterWsMessageArguments, CanisterWsOpenArguments, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, wsMessageIdl, wsOpenIdl } from "./idl"; +import { CanisterWsMessageArguments, CanisterWsOpenArguments, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, isClientKeyEq, wsMessageIdl, wsOpenIdl } from "./idl"; import { canisterId, client1Key } from "./test/clients"; -import { INVALID_MESSAGE_KEY, VALID_ACK_MESSAGE, VALID_MESSAGE_SEQ_NUM_2, VALID_MESSAGE_SEQ_NUM_3, VALID_OPEN_MESSAGE } from "./test/messages"; +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 { getTestCanisterActor, getTestCanisterActorWithoutMethods, getTestCanisterActorWrongArgs, getTestCanisterActorWrongOpt } from "./test/actor"; import type { WsAgentRequestMessage } from "./agent/types"; import { GATEWAY_PRINCIPAL } from "./test/constants"; +import { GatewayHandshakeMessage } from "./types"; const wsGatewayAddress = "ws://127.0.0.1:8080"; // the canister from which the application message was sent (needed to verify the message certificate) @@ -23,7 +24,6 @@ const testCanisterActor = getTestCanisterActor(canisterId); const icWebsocketConfig = createWsConfig({ canisterId: canisterId.toText(), - gatewayPrincipal: GATEWAY_PRINCIPAL.toText(), canisterActor: testCanisterActor, networkUrl: icNetworkUrl, identity: generateRandomIdentity(), @@ -44,6 +44,11 @@ const mockReplica = setupServer( ); mockReplica.listen(); +const sendHandshakeMessage = async (message: GatewayHandshakeMessage) => { + mockWsServer.send(encodeHandshakeMessage(message)); + await sleep(100); +}; + describe("IcWebsocket class", () => { beforeEach(() => { mockWsServer = new WsMockServer(wsGatewayAddress); @@ -78,17 +83,6 @@ describe("IcWebsocket class", () => { expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); }); - it("throws an error if the gatewayPrincipal is not provided or invalid", () => { - let icWsConfig = createWsConfig({ ...icWebsocketConfig }); - // @ts-ignore - delete icWsConfig.gatewayPrincipal; - expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); - - icWsConfig = createWsConfig({ ...icWebsocketConfig }); - icWsConfig.gatewayPrincipal = "invalid"; - expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError(); - }); - it("passes if the canisterId is a valid string or principal", () => { let icWsConfig = createWsConfig({ ...icWebsocketConfig }); expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); @@ -98,15 +92,6 @@ describe("IcWebsocket class", () => { expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); }); - it("passes if the gatewayPrincipal is a valid string or principal", () => { - let icWsConfig = createWsConfig({ ...icWebsocketConfig }); - expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); - - icWsConfig = createWsConfig({ ...icWebsocketConfig }); - icWsConfig.gatewayPrincipal = GATEWAY_PRINCIPAL; - expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); - }); - it("throws an error if the canisterActor is not provided", () => { const icWsConfig = createWsConfig({ ...icWebsocketConfig }); // @ts-ignore @@ -153,10 +138,49 @@ describe("IcWebsocket class", () => { expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError("Network url is required"); }); + it("throws an error if the handshake message is wrong", async () => { + const onOpen = jest.fn(); + const onError = jest.fn(); + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); + expect(icWs).toBeDefined(); + icWs.onopen = onOpen; + icWs.onerror = onError; + await mockWsServer.connected; + + await sendHandshakeMessage(INVALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); + + expect(onOpen).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalled(); + expect(icWs["_isHandshakeCompleted"]).toEqual(false); + expect(icWs["_gatewayPrincipal"]).toBeNull(); + expect(icWs["_clientKey"]).toBeNull(); + expect(icWs["_isConnectionEstablished"]).toEqual(false); + }); + + it("completes the handshake with a valid handshake message", async () => { + const onOpen = jest.fn(); + const onError = jest.fn(); + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); + expect(icWs).toBeDefined(); + icWs.onopen = onOpen; + icWs.onerror = onError; + await mockWsServer.connected; + + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); + + expect(onOpen).not.toHaveBeenCalled(); + expect(onError).not.toHaveBeenCalled(); + expect(icWs["_isHandshakeCompleted"]).toEqual(true); + expect(icWs["_gatewayPrincipal"]).toEqual(GATEWAY_PRINCIPAL); + expect(icWs["_clientKey"]).not.toBeNull(); + expect(icWs["_isConnectionEstablished"]).toEqual(false); + }); + it("creates a new instance and sends the open message", async () => { const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); // get the open message sent by the client from the mock websocket server const openMessageBytes = await mockWsServer.nextMessage as ArrayBuffer; @@ -186,6 +210,7 @@ describe("IcWebsocket class", () => { icWs.onopen = onOpen; icWs.onmessage = onMessage; await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); expect(onOpen).not.toHaveBeenCalled(); expect(icWs["_isConnectionEstablished"]).toEqual(false); @@ -214,6 +239,7 @@ describe("IcWebsocket class", () => { 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 @@ -247,6 +273,7 @@ describe("IcWebsocket class", () => { 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 @@ -282,6 +309,7 @@ describe("IcWebsocket class", () => { 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 @@ -311,6 +339,7 @@ describe("IcWebsocket class", () => { const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); expect(() => icWs.send({ text: "test" })).toThrowError("Connection is not established yet"); }); @@ -319,6 +348,7 @@ describe("IcWebsocket class", () => { const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); expect(icWs).toBeDefined(); await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); // wait for the open message from the client await mockWsServer.nextMessage; @@ -386,6 +416,7 @@ describe("Messages acknowledgement", () => { icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); // wait for the open message from the client await mockWsServer.nextMessage; @@ -428,6 +459,7 @@ describe("Messages acknowledgement", () => { icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); // wait for the open message from the client await mockWsServer.nextMessage; @@ -474,6 +506,7 @@ describe("Messages acknowledgement", () => { icWs.onerror = onError; icWs.onclose = onClose; await mockWsServer.connected; + await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); // wait for the open message from the client await mockWsServer.nextMessage; diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index 5b4f997..0e0ffb8 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -25,6 +25,8 @@ import { isMessageBodyValid, randomBigInt, safeExecute } from "./utils"; import { isClientIncomingMessage, type ClientIncomingMessage, + isGatewayHandshakeMessage, + type GatewayHandshakeMessage, } from "./types"; import { callCanisterWsMessage, callCanisterWsOpen } from "./actor"; import { @@ -47,10 +49,6 @@ export interface IcWebSocketConfig { * The canister id of the canister to open the WebSocket to. */ canisterId: string | Principal; - /** - * The principal of the gateway to which the WebSocket will be opened. - */ - gatewayPrincipal: string | Principal; /** * The canister actor used to serialize and deserialize the application messages. */ @@ -90,7 +88,6 @@ export default class IcWebSocket< ApplicationMessageType = GetApplicationMessageType > { public readonly canisterId: Principal; - public readonly gatewayPrincipal: Principal; private readonly _canisterActor: ActorSubclass; private readonly _applicationMessageIdl: IDL.Type; private readonly _httpAgent: HttpAgent; @@ -99,11 +96,13 @@ export default class IcWebSocket< private readonly _identity: Promise; private _incomingSequenceNum = BigInt(1); private _outgoingSequenceNum = BigInt(0); + private _isHandshakeCompleted = false; private _isConnectionEstablished = false; private _incomingMessagesQueue: BaseQueue; private _outgoingMessagesQueue: BaseQueue; private _ackMessagesQueue: AckMessagesQueue; private _clientKey: ClientKey | null = null; + private _gatewayPrincipal: Principal | null = null; private _maxCertificateAgeInMinutes = 5; onclose: ((this: IcWebSocket, ev: CloseEvent) => any) | null = null; @@ -132,7 +131,6 @@ export default class IcWebSocket< */ constructor(url: WsParameters[0], protocols: WsParameters[1], config: IcWebSocketConfig) { this.canisterId = Principal.from(config.canisterId); - this.gatewayPrincipal = Principal.from(config.gatewayPrincipal); if (!config.canisterActor) { throw new Error("Canister actor is required"); @@ -214,6 +212,34 @@ export default class IcWebSocket< } private async _onWsOpen() { + this._incomingMessagesQueue.enableAndProcess(); + + logger.debug("[onWsOpen] WebSocket opened"); + } + + private _onWsMessage(event: MessageEvent) { + this._incomingMessagesQueue.addAndProcess(event.data); + } + + private async _handleHandshakeMessage(handshakeMessage: GatewayHandshakeMessage): Promise { + // at this point, we're sure that the gateway_principal is valid + // because the isGatewayHandshakeMessage function checks it + this._gatewayPrincipal = Principal.from(handshakeMessage.gateway_principal); + this._isHandshakeCompleted = true; + + try { + await this._sendOpenMessage(this._gatewayPrincipal); + } catch (error) { + logger.error("[onWsMessage] Handshake message error:", error); + // if a handshake message fails, we can't continue + this._wsInstance.close(4000, "Handshake message error"); + return false; + } + + return true; + } + + private async _sendOpenMessage(gatewayPrincipal: Principal) { this._clientKey = { client_principal: await this.getPrincipal(), client_nonce: randomBigInt(), @@ -225,39 +251,37 @@ export default class IcWebSocket< ws: this._wsInstance, }); - logger.debug("[onWsOpen] WebSocket opened, sending open message"); - - try { - // Call the canister's ws_open method - await callCanisterWsOpen( - this.canisterId, - this._wsAgent, - { - client_nonce: this._clientKey.client_nonce, - gateway_principal: this.gatewayPrincipal, - } - ); - - this._incomingMessagesQueue.enableAndProcess(); + logger.debug("Sending open message"); - logger.debug("[onWsOpen] Open message sent, waiting for first open message from canister"); - } catch (error) { - logger.error("[onWsOpen] Error:", error); - // if the first message fails, we can't continue - this._wsInstance.close(4000, "First message failed"); - } - } + // Call the canister's ws_open method + await callCanisterWsOpen( + this.canisterId, + this._wsAgent, + { + client_nonce: this._clientKey.client_nonce, + gateway_principal: gatewayPrincipal, + } + ); - private _onWsMessage(event: MessageEvent) { - this._incomingMessagesQueue.addAndProcess(event.data); + logger.debug("Open message sent, waiting for first open message from canister"); } private async _processIncomingMessage(message: ArrayBuffer): Promise { try { const incomingMessage = this._decodeIncomingMessage(message); + + // if the handshake is not completed yet, we have to treat the first message as HandshakeMessage + if (!this._isHandshakeCompleted) { + if (!isGatewayHandshakeMessage(incomingMessage)) { + throw new Error("First message is not a GatewayHandshakeMessage"); + } + + return this._handleHandshakeMessage(incomingMessage); + } + // Check if the incoming message is a ClientIncomingMessage if (!isClientIncomingMessage(incomingMessage)) { - throw new Error("[onWsMessage] Incoming message is not a ClientIncomingMessage, ignoring message"); + throw new Error("Incoming message is not a ClientIncomingMessage"); } logger.debug("[onWsMessage] Incoming message received. Bytes:", message.byteLength, "bytes"); @@ -287,7 +311,7 @@ export default class IcWebSocket< await this._callOnMessageCallback(new Uint8Array(websocketMessage.content)); } catch (error) { // for any error, we can't continue - logger.error("[onWsMessage] Error:", error); + logger.error("[onWsMessage]", error); this._callOnErrorCallback(new Error(`Error receiving message: ${error}`)); this._wsInstance.close(4000, "Error receiving message"); return false; @@ -375,7 +399,7 @@ export default class IcWebSocket< } private _onWsError(error: Event) { - logger.error("[onWsError] Error:", error); + logger.error("[onWsError]", error); this._callOnErrorCallback(new Error(`WebSocket error: ${error}`)); } @@ -415,7 +439,13 @@ export default class IcWebSocket< return true; } - private _decodeIncomingMessage(buf: ArrayBuffer): ClientIncomingMessage { + /** + * CBOR decodes the incoming message from an ArrayBuffer and returns an object. + * + * @param {ArrayBuffer} buf - The ArrayBuffer containing the encoded message. + * @returns {any} The decoded object. + */ + private _decodeIncomingMessage(buf: ArrayBuffer): any { return Cbor.decode(buf); } diff --git a/src/test/messages.ts b/src/test/messages.ts index f84991d..c6f392c 100644 --- a/src/test/messages.ts +++ b/src/test/messages.ts @@ -1,9 +1,22 @@ -import { fromHex } from "@dfinity/agent"; -import { ClientIncomingMessage } from "../types"; +import { Cbor, fromHex } from "@dfinity/agent"; +import { ClientIncomingMessage, GatewayHandshakeMessage } from "../types"; import { GATEWAY_PRINCIPAL } from "./constants"; -// Messages generated from a canister running in a local replica +// Messages generated from a gateway +export const VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY: GatewayHandshakeMessage = { + gateway_principal: GATEWAY_PRINCIPAL, +}; + +export const INVALID_HANDSHAKE_MESSAGE_FROM_GATEWAY: GatewayHandshakeMessage = { + // @ts-ignore + gateway_principal: "", +}; + +export const encodeHandshakeMessage = (message: GatewayHandshakeMessage): ArrayBuffer => { + return Cbor.encode(message); +} +// Messages generated from a canister running in a local replica export const VALID_OPEN_MESSAGE: ClientIncomingMessage = { key: `${GATEWAY_PRINCIPAL}_00000000000000000000`, content: new Uint8Array(fromHex("d9d9f7a56a636c69656e745f6b6579a270636c69656e745f7072696e636970616c581d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e026c636c69656e745f6e6f6e63651b500eeedc6a0443246c73657175656e63655f6e756d016974696d657374616d701b178e8b8e33b7a68b7269735f736572766963655f6d657373616765f567636f6e74656e7458614449444c046b03fdbd95cc0101bfd397b409039eb7f0ad0b036c01ebb49ce903026c02fa80a2940568bbd1eacd0e786c01d888abb90a78010000011d335a83e713c14f0abf2fa5c1fe2d1e4ef23e94674709a6efbd102c7e022443046adcee0e50")), diff --git a/src/types.test.ts b/src/types.test.ts new file mode 100644 index 0000000..79be37b --- /dev/null +++ b/src/types.test.ts @@ -0,0 +1,88 @@ +import { Principal } from "@dfinity/principal"; +import { isClientIncomingMessage, isGatewayHandshakeMessage } from "./types"; + +describe("isClientIncomingMessage", () => { + it("should return false for wrong type", () => { + const incomingMessageWrongType = "not a ClientIncomingMessage"; + expect(isClientIncomingMessage(incomingMessageWrongType)).toBe(false); + }) + + it("should return false for wrong properties", () => { + const incomingMessageWrongKey = { + key: {}, + content: new Uint8Array(), + cert: new Uint8Array(), + tree: new Uint8Array() + }; + const incomingMessageWrongContent = { + key: "key", + content: "not a Uint8Array", + cert: new Uint8Array(), + tree: new Uint8Array() + }; + const incomingMessageWrongCert = { + key: "key", + content: new Uint8Array(), + cert: "not a Uint8Array", + tree: new Uint8Array() + }; + const incomingMessageWrongTree = { + key: "key", + content: new Uint8Array(), + cert: new Uint8Array(), + tree: "not a Uint8Array" + } + + expect(isClientIncomingMessage(incomingMessageWrongKey)).toBe(false); + expect(isClientIncomingMessage(incomingMessageWrongContent)).toBe(false); + expect(isClientIncomingMessage(incomingMessageWrongCert)).toBe(false); + expect(isClientIncomingMessage(incomingMessageWrongTree)).toBe(false); + }) + + it("should return true for valid client incoming message", () => { + const incomingMessage = { + key: "key", + content: new Uint8Array(), + cert: new Uint8Array(), + tree: new Uint8Array() + }; + + expect(isClientIncomingMessage(incomingMessage)).toBe(true); + }); +}); + +describe("isGatewayHandshakeMessage", () => { + it("should return false for wrong type", () => { + const handshakeMessageWrongType = "not a HandshakeMessage"; + + expect(isGatewayHandshakeMessage(handshakeMessageWrongType)).toBe(false); + }); + + it("should return false for wrong properties", () => { + const handshakeMessageWrongGatewayPrincipal1 = { + gateway_principal: {}, + }; + const handshakeMessageWrongGatewayPrincipal2 = { + gateway_principal: "", + }; + const handshakeMessageWrongGatewayPrincipal3 = { + gateway_principal: null, + }; + + expect(isGatewayHandshakeMessage(handshakeMessageWrongGatewayPrincipal1)).toBe(false); + expect(isGatewayHandshakeMessage(handshakeMessageWrongGatewayPrincipal2)).toBe(false); + expect(isGatewayHandshakeMessage(handshakeMessageWrongGatewayPrincipal3)).toBe(false); + }); + + it("should return true for valid handshake message", () => { + const message = { + gateway_principal: Principal.fromText("pmisz-prtlk-b6oe6-bj4fl-6l5fy-h7c2h-so6i7-jiz2h-bgto7-piqfr-7ae"), // a random but valid principal + }; + const message2 = { + gateway_principal: Principal.fromText("pmisz-prtlk-b6oe6-bj4fl-6l5fy-h7c2h-so6i7-jiz2h-bgto7-piqfr-7ae").toUint8Array(), + }; + + expect(isGatewayHandshakeMessage(message)).toBe(true); + expect(isGatewayHandshakeMessage(message2)).toBe(true); + }); +}); diff --git a/src/types.ts b/src/types.ts index 05ee3d9..451ec8f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,7 @@ import { ActorMethod } from "@dfinity/agent"; +import { Principal } from "@dfinity/principal"; + +export type GetInnerType = S extends ActorMethod ? T : never; export type ClientIncomingMessage = { key: string; @@ -6,7 +9,6 @@ export type ClientIncomingMessage = { cert: Uint8Array; tree: Uint8Array; }; - export const isClientIncomingMessage = (arg: unknown): arg is ClientIncomingMessage => { return ( arg instanceof Object && @@ -17,4 +19,30 @@ export const isClientIncomingMessage = (arg: unknown): arg is ClientIncomingMess ); }; -export type GetInnerType = S extends ActorMethod ? T : never; +const isPrincipal = (arg: unknown): arg is Principal => { + // the Principal.from method doesn't throw if the argument is a string, + // but in our case it must already be a Principal instance + // see https://github.com/dfinity/agent-js/blob/349598672c50d738100d123a43f5d1c8fac77854/packages/principal/src/index.ts#L39-L53 + if (typeof arg === "string") { + return false; + } + + try { + Principal.from(arg); + } catch (e) { + console.error("isPrincipal", e); + return false; + } + + return true; +}; + +export type GatewayHandshakeMessage = { + gateway_principal: Principal | Uint8Array; +}; +export const isGatewayHandshakeMessage = (arg: unknown): arg is GatewayHandshakeMessage => { + return ( + arg instanceof Object && + isPrincipal((arg as GatewayHandshakeMessage).gateway_principal) + ); +}; From 8fb0a3b6008c0600cc3f7de558985a29186e02de Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 17 Nov 2023 20:30:02 +0100 Subject: [PATCH 5/8] chore: update @dfinity dependencies to v0.20.1 --- package-lock.json | 40 ++++++++++++++++++++-------------------- package.json | 8 ++++---- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9347b18..f532bf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,10 +39,10 @@ "node": "^14 || ^16 || ^18" }, "peerDependencies": { - "@dfinity/agent": "^0.20.0", - "@dfinity/candid": "^0.20.0", - "@dfinity/identity-secp256k1": "^0.20.0", - "@dfinity/principal": "^0.20.0" + "@dfinity/agent": "^0.20.1", + "@dfinity/candid": "^0.20.1", + "@dfinity/identity-secp256k1": "^0.20.1", + "@dfinity/principal": "^0.20.1" } }, "node_modules/@ampproject/remapping": { @@ -2335,9 +2335,9 @@ } }, "node_modules/@dfinity/agent": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.20.0.tgz", - "integrity": "sha512-KXCBGEUxyCtkJ4IkpVo+hKIfjILWKatqbWeTME9VdaTiRp0aEhOsqlMSAih5BC0nfaNqHVmNLo6W3lYVJpL1Mg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.20.1.tgz", + "integrity": "sha512-Ykk0eaFz8b9f94HldOvDCyBErwJTFY3x1S3yEPEtTgeS2UDZolezVzYgOt/vcjtpGcJvM/kQDSGCvOJNlf58NQ==", "peer": true, "dependencies": { "@noble/curves": "^1.2.0", @@ -2348,26 +2348,26 @@ "simple-cbor": "^0.4.1" }, "peerDependencies": { - "@dfinity/candid": "^0.20.0", - "@dfinity/principal": "^0.20.0" + "@dfinity/candid": "^0.20.1", + "@dfinity/principal": "^0.20.1" } }, "node_modules/@dfinity/candid": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.20.0.tgz", - "integrity": "sha512-GoWmNtop+Jbd3IgZHbyz3BG2CO4yvLrW/1k7+1QMqDMWAYFUe+K87G2jPqH5Fmt3ftkzjzCgFpuH0DqSFCCTPA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.20.1.tgz", + "integrity": "sha512-o8g/GOoAAbWr7uY8111nKd+6ARGTsy9viGjVkgzUg9Tpfx5KY0HSap2c1JJ00YkIxjb3dlihL8Xs8a/N7Jp+/A==", "peer": true, "peerDependencies": { - "@dfinity/principal": "^0.20.0" + "@dfinity/principal": "^0.20.1" } }, "node_modules/@dfinity/identity-secp256k1": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@dfinity/identity-secp256k1/-/identity-secp256k1-0.20.0.tgz", - "integrity": "sha512-UCFncyEvCZo/TrgsKNsSKIcx6ZEYY9BbUZ6cvack0TJn6FYdjLoIihbzDlpfMWDM9iJ9J1a56Iqrl7WX4ENPJg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@dfinity/identity-secp256k1/-/identity-secp256k1-0.20.1.tgz", + "integrity": "sha512-KH4/V9AsTYQK+1tQrpr3LCVkEI/pmuK5vSW1e68K7VHxjSvRUiM7OV4EH6x+B92JC7eIErNILPIyi0PoTL5BvA==", "peer": true, "dependencies": { - "@dfinity/agent": "^0.20.0", + "@dfinity/agent": "^0.20.1", "@noble/hashes": "^1.3.1", "bip39": "^3.1.0", "bs58check": "^3.0.1", @@ -2422,9 +2422,9 @@ } }, "node_modules/@dfinity/principal": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.20.0.tgz", - "integrity": "sha512-g64nQHUP0i/HNqS/iQb+SXstKWy0Mk4b3XhXqoHLTh2+S04M9CCKmEtq7h6ryVfa0lUhvTdaDKnrsmRHzP0czQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.20.1.tgz", + "integrity": "sha512-R+l9QMunqZAo38+khc0kb7l3EUVqIb28lD8pI5tjDc0CFCvqoe3jbog2SH4z1TrLBmXuPeI5hteMT/J+R+K+7Q==", "peer": true, "dependencies": { "@noble/hashes": "^1.3.1" diff --git a/package.json b/package.json index 740fc06..a930688 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,9 @@ "loglevel": "^1.8.1" }, "peerDependencies": { - "@dfinity/agent": "^0.20.0", - "@dfinity/candid": "^0.20.0", - "@dfinity/identity-secp256k1": "^0.20.0", - "@dfinity/principal": "^0.20.0" + "@dfinity/agent": "^0.20.1", + "@dfinity/candid": "^0.20.1", + "@dfinity/identity-secp256k1": "^0.20.1", + "@dfinity/principal": "^0.20.1" } } From 38ed1737e68672a07170ff8cd3d00539045d9a3d Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Fri, 17 Nov 2023 20:36:34 +0100 Subject: [PATCH 6/8] chore: update actions to v4 --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c48406..d6e16d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,8 +11,8 @@ jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 cache: "npm" From c6524577d1901860988b2237c2d62b1d5510dc1f Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Tue, 21 Nov 2023 10:04:17 +0100 Subject: [PATCH 7/8] perf: initialize functions and minor renames --- src/ic-websocket.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index 0e0ffb8..1610f6b 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -228,7 +228,7 @@ export default class IcWebSocket< this._isHandshakeCompleted = true; try { - await this._sendOpenMessage(this._gatewayPrincipal); + await this._sendOpenMessage(); } catch (error) { logger.error("[onWsMessage] Handshake message error:", error); // if a handshake message fails, we can't continue @@ -239,27 +239,35 @@ export default class IcWebSocket< return true; } - private async _sendOpenMessage(gatewayPrincipal: Principal) { + private async _initializeClientKey() { this._clientKey = { client_principal: await this.getPrincipal(), client_nonce: randomBigInt(), - } + }; + } + private _initializeWsAgent() { this._wsAgent = new WsAgent({ identity: this._identity, httpAgent: this._httpAgent, ws: this._wsInstance, }); + } + + private async _sendOpenMessage() { + await this._initializeClientKey(); + this._initializeWsAgent(); logger.debug("Sending open message"); // Call the canister's ws_open method + // at this point, all the class properties that we need are initialized await callCanisterWsOpen( this.canisterId, - this._wsAgent, + this._wsAgent!, { - client_nonce: this._clientKey.client_nonce, - gateway_principal: gatewayPrincipal, + client_nonce: this._clientKey!.client_nonce, + gateway_principal: this._gatewayPrincipal!, } ); @@ -373,7 +381,7 @@ export default class IcWebSocket< }); const keepAliveMessage = this._makeWsMessageArguments(new Uint8Array(bytes), true); - const sent = await this._sendMessage(keepAliveMessage); + const sent = await this._sendMessageToCanister(keepAliveMessage); if (!sent) { logger.error("[onWsMessage] Keep alive message was not sent"); this._callOnErrorCallback(new Error("Keep alive message was not sent")); @@ -406,7 +414,7 @@ export default class IcWebSocket< private _sendMessageFromQueue(messageContent: Uint8Array): Promise { const message = this._makeWsMessageArguments(messageContent!); // we send the message via WebSocket to the gateway, which relays it to the canister - return this._sendMessage(message); + return this._sendMessageToCanister(message); } /** @@ -414,7 +422,7 @@ export default class IcWebSocket< * @param message * @returns {boolean} `true` if the message was sent successfully, `false` otherwise. */ - private async _sendMessage(message: CanisterWsMessageArguments): Promise { + private async _sendMessageToCanister(message: CanisterWsMessageArguments): Promise { // we don't need to wait for the response, // as we'll receive the ack message via WebSocket from the canister try { @@ -439,7 +447,7 @@ export default class IcWebSocket< return true; } - /** + /** * CBOR decodes the incoming message from an ArrayBuffer and returns an object. * * @param {ArrayBuffer} buf - The ArrayBuffer containing the encoded message. From 2d217685eaed9d6687080f8d2338aa3f8e917618 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Tue, 21 Nov 2023 10:35:08 +0100 Subject: [PATCH 8/8] fix: accept only SignIdentity accepting a promise of the identity makes the constructor always fail, so we remove it for now --- src/ic-websocket.test.ts | 47 ++++++++++++++++++++++++++++++---------- src/ic-websocket.ts | 31 ++++++++++++-------------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/ic-websocket.test.ts b/src/ic-websocket.test.ts index 894d559..1fab560 100644 --- a/src/ic-websocket.test.ts +++ b/src/ic-websocket.test.ts @@ -7,7 +7,7 @@ import { IDL } from "@dfinity/candid"; import IcWebSocket, { createWsConfig } from "./ic-websocket"; import { Principal } from "@dfinity/principal"; import { generateRandomIdentity } from "./identity"; -import { CanisterWsMessageArguments, CanisterWsOpenArguments, WebsocketServiceMessageContent, _WS_CANISTER_SERVICE, decodeWebsocketServiceMessageContent, isClientKeyEq, wsMessageIdl, wsOpenIdl } from "./idl"; +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"; @@ -126,10 +126,24 @@ describe("IcWebsocket class", () => { const icWsConfig = createWsConfig({ ...icWebsocketConfig }); // @ts-ignore icWsConfig.identity = {}; + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError("Identity must be a SignIdentity"); + const icWsConfig2 = createWsConfig({ ...icWebsocketConfig }); + // @ts-ignore + icWsConfig2.identity = Promise.resolve({}); + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError("Identity must be a SignIdentity"); + + const icWsConfig3 = createWsConfig({ ...icWebsocketConfig }); + // @ts-ignore + icWsConfig3.identity = Promise.resolve(generateRandomIdentity()); expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError("Identity must be a SignIdentity"); }); + it("passes if the identity is a SignIdentity", () => { + const icWsConfig = createWsConfig({ ...icWebsocketConfig }); + expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).not.toThrowError(); + }); + it("throws an error if the networkUrl is not provided", () => { const icWsConfig = createWsConfig({ ...icWebsocketConfig }); // @ts-ignore @@ -138,6 +152,19 @@ describe("IcWebsocket class", () => { expect(() => new IcWebSocket(wsGatewayAddress, undefined, icWsConfig)).toThrowError("Network url is required"); }); + it("creates a new client key correctly", () => { + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); + expect(icWs["_clientKey"]).toMatchObject({ + client_principal: icWebsocketConfig.identity.getPrincipal(), + client_nonce: expect.any(BigInt), + }); + }); + + it("getPrincipal returns the correct principal", () => { + const icWs = new IcWebSocket(wsGatewayAddress, undefined, icWebsocketConfig); + expect(icWs.getPrincipal().compareTo(icWebsocketConfig.identity.getPrincipal())).toEqual("eq"); + }); + it("throws an error if the handshake message is wrong", async () => { const onOpen = jest.fn(); const onError = jest.fn(); @@ -153,7 +180,6 @@ describe("IcWebsocket class", () => { expect(onError).toHaveBeenCalled(); expect(icWs["_isHandshakeCompleted"]).toEqual(false); expect(icWs["_gatewayPrincipal"]).toBeNull(); - expect(icWs["_clientKey"]).toBeNull(); expect(icWs["_isConnectionEstablished"]).toEqual(false); }); @@ -172,7 +198,6 @@ describe("IcWebsocket class", () => { expect(onError).not.toHaveBeenCalled(); expect(icWs["_isHandshakeCompleted"]).toEqual(true); expect(icWs["_gatewayPrincipal"]).toEqual(GATEWAY_PRINCIPAL); - expect(icWs["_clientKey"]).not.toBeNull(); expect(icWs["_isConnectionEstablished"]).toEqual(false); }); @@ -186,7 +211,7 @@ describe("IcWebsocket class", () => { const openMessageBytes = await mockWsServer.nextMessage as ArrayBuffer; // reconstruct the message that the client should send - const clientKey = icWs["_clientKey"]!; + const clientKey = icWs["_clientKey"]; const { envelope: { content: openMessageContent } }: WsAgentRequestMessage = Cbor.decode(openMessageBytes); expect(canisterId.compareTo( @@ -241,7 +266,7 @@ describe("IcWebsocket class", () => { await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister @@ -275,7 +300,7 @@ describe("IcWebsocket class", () => { await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister @@ -311,7 +336,7 @@ describe("IcWebsocket class", () => { await mockWsServer.connected; await sendHandshakeMessage(VALID_HANDSHAKE_MESSAGE_FROM_GATEWAY); - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister @@ -353,7 +378,7 @@ describe("IcWebsocket class", () => { // wait for the open message from the client await mockWsServer.nextMessage; - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister @@ -421,7 +446,7 @@ describe("Messages acknowledgement", () => { // wait for the open message from the client await mockWsServer.nextMessage; - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister @@ -464,7 +489,7 @@ describe("Messages acknowledgement", () => { // wait for the open message from the client await mockWsServer.nextMessage; - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister @@ -511,7 +536,7 @@ describe("Messages acknowledgement", () => { // wait for the open message from the client await mockWsServer.nextMessage; - const originalClientKey = { ...icWs["_clientKey"]! }; + const originalClientKey = { ...icWs["_clientKey"] }; // workaround to simulate the client identity icWs["_clientKey"] = client1Key; // send the open confirmation message from the canister diff --git a/src/ic-websocket.ts b/src/ic-websocket.ts index 1610f6b..f3c1191 100644 --- a/src/ic-websocket.ts +++ b/src/ic-websocket.ts @@ -56,7 +56,7 @@ export interface IcWebSocketConfig { /** * The identity to use for signing messages. If empty, a new random temporary identity will be generated. */ - identity: SignIdentity | Promise, + identity: SignIdentity, /** * The IC network url to use for the underlying agent. It can be a local replica URL (e.g. http://localhost:4943) or the IC mainnet URL (https://icp0.io). */ @@ -93,7 +93,7 @@ export default class IcWebSocket< private readonly _httpAgent: HttpAgent; private _wsAgent: WsAgent | null = null; private readonly _wsInstance: WebSocket; - private readonly _identity: Promise; + private readonly _identity: SignIdentity; private _incomingSequenceNum = BigInt(1); private _outgoingSequenceNum = BigInt(0); private _isHandshakeCompleted = false; @@ -101,7 +101,7 @@ export default class IcWebSocket< private _incomingMessagesQueue: BaseQueue; private _outgoingMessagesQueue: BaseQueue; private _ackMessagesQueue: AckMessagesQueue; - private _clientKey: ClientKey | null = null; + private _clientKey: ClientKey; private _gatewayPrincipal: Principal | null = null; private _maxCertificateAgeInMinutes = 5; @@ -144,7 +144,12 @@ export default class IcWebSocket< if (!(config.identity instanceof SignIdentity)) { throw new Error("Identity must be a SignIdentity"); } - this._identity = Promise.resolve(config.identity); + this._identity = config.identity; + + this._clientKey = { + client_principal: this.getPrincipal(), + client_nonce: randomBigInt(), + }; if (!config.networkUrl) { throw new Error("Network url is required"); @@ -192,8 +197,8 @@ export default class IcWebSocket< this._outgoingMessagesQueue.addAndProcess(new Uint8Array(data)); } - public async getPrincipal(): Promise { - return (await this._identity).getPrincipal(); + public getPrincipal(): Principal { + return this._identity.getPrincipal(); } public close() { @@ -239,13 +244,6 @@ export default class IcWebSocket< return true; } - private async _initializeClientKey() { - this._clientKey = { - client_principal: await this.getPrincipal(), - client_nonce: randomBigInt(), - }; - } - private _initializeWsAgent() { this._wsAgent = new WsAgent({ identity: this._identity, @@ -255,7 +253,6 @@ export default class IcWebSocket< } private async _sendOpenMessage() { - await this._initializeClientKey(); this._initializeWsAgent(); logger.debug("Sending open message"); @@ -266,7 +263,7 @@ export default class IcWebSocket< this.canisterId, this._wsAgent!, { - client_nonce: this._clientKey!.client_nonce, + client_nonce: this._clientKey.client_nonce, gateway_principal: this._gatewayPrincipal!, } ); @@ -333,7 +330,7 @@ export default class IcWebSocket< const serviceMessage = decodeWebsocketServiceMessageContent(content as Uint8Array); if ("OpenMessage" in serviceMessage) { logger.debug("[onWsMessage] Received open message from canister"); - if (!isClientKeyEq(serviceMessage.OpenMessage.client_key, this._clientKey!)) { + if (!isClientKeyEq(serviceMessage.OpenMessage.client_key, this._clientKey)) { throw new Error("Client key does not match"); } @@ -499,7 +496,7 @@ export default class IcWebSocket< this._outgoingSequenceNum++; const outgoingMessage: WebsocketMessage = { - client_key: this._clientKey!, + client_key: this._clientKey, sequence_num: this._outgoingSequenceNum, timestamp: BigInt(Date.now()) * BigInt(10 ** 6), content,