From f0e17976e97a0761d5e3f582b218b4894bacb10a Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Wed, 17 Jan 2024 11:38:33 +0200 Subject: [PATCH 01/36] fix: adds additional checks for session validity state such as confirming keychain exists and removes session if deemed invalid --- .../sign-client/src/controllers/engine.ts | 41 +++++++++++++------ packages/types/src/sign-client/engine.ts | 7 +++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index 3453dfcfa..9e85feb26 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -119,7 +119,6 @@ export class Engine extends IEngine { this.registerPairingEvents(); this.client.core.pairing.register({ methods: Object.keys(ENGINE_RPC_OPTS) }); this.initialized = true; - setTimeout(() => { this.sessionRequestQueue.queue = this.getPendingSessionRequests(); this.processSessionRequestQueue(); @@ -434,9 +433,15 @@ export class Engine extends IEngine { params: getSdkError("USER_DISCONNECTED"), throwOnFailedPublish: true, }); - await this.deleteSession(topic); - } else { + await this.deleteSession({ topic, emitEvent: false }); + } else if (this.client.core.pairing.pairings.keys.includes(topic)) { await this.client.core.pairing.disconnect({ topic }); + } else { + const { message } = getInternalError( + "MISMATCHED_TOPIC", + `No Session or Pairing topic found: ${topic}`, + ); + throw new Error(message); } }; @@ -446,7 +451,6 @@ export class Engine extends IEngine { }; public getPendingSessionRequests: IEngine["getPendingSessionRequests"] = () => { - this.isInitialized(); return this.client.pendingRequest.getAll(); }; @@ -479,11 +483,12 @@ export class Engine extends IEngine { } }; - private deleteSession: EnginePrivate["deleteSession"] = async (topic, expirerHasDeleted) => { + private deleteSession: EnginePrivate["deleteSession"] = async (params) => { + const { topic, expirerHasDeleted = false, emitEvent = true, id = 0 } = params; const { self } = this.client.session.get(topic); // Await the unsubscribe first to avoid deleting the symKey too early below. await this.client.core.relayer.unsubscribe(topic); - this.client.session.delete(topic, getSdkError("USER_DISCONNECTED")); + await this.client.session.delete(topic, getSdkError("USER_DISCONNECTED")); if (this.client.core.crypto.keychain.has(self.publicKey)) { await this.client.core.crypto.deleteKeyPair(self.publicKey); } @@ -501,6 +506,7 @@ export class Engine extends IEngine { this.deletePendingSessionRequest(r.id, getSdkError("USER_DISCONNECTED")); } }); + if (emitEvent) this.client.events.emit("session_delete", { id, topic }); }; private deleteProposal: EnginePrivate["deleteProposal"] = async (id, expirerHasDeleted) => { @@ -612,13 +618,16 @@ export class Engine extends IEngine { const sessionTopics: string[] = []; const proposalIds: number[] = []; this.client.session.getAll().forEach((session) => { - if (isExpired(session.expiry)) sessionTopics.push(session.topic); + let toCleanup = false; + if (isExpired(session.expiry)) toCleanup = true; + if (!this.client.core.crypto.keychain.has(session.topic)) toCleanup = true; + if (toCleanup) sessionTopics.push(session.topic); }); this.client.proposal.getAll().forEach((proposal) => { if (isExpired(proposal.expiry)) proposalIds.push(proposal.id); }); await Promise.all([ - ...sessionTopics.map((topic) => this.deleteSession(topic)), + ...sessionTopics.map((topic) => this.deleteSession({ topic })), ...proposalIds.map((id) => this.deleteProposal(id)), ]); }; @@ -969,12 +978,11 @@ export class Engine extends IEngine { new Promise((resolve) => { // RPC request needs to happen before deletion as it utalises session encryption this.client.core.relayer.once(RELAYER_EVENTS.publish, async () => { - resolve(await this.deleteSession(topic)); + resolve(await this.deleteSession({ topic, id })); }); }), this.sendResult<"wc_sessionDelete">({ id, topic, result: true }), ]); - this.client.events.emit("session_delete", { id, topic }); } catch (err: any) { this.client.logger.error(err); } @@ -1081,7 +1089,7 @@ export class Engine extends IEngine { if (topic) { if (this.client.session.keys.includes(topic)) { - await this.deleteSession(topic, true); + await this.deleteSession({ topic, expirerHasDeleted: true }); this.client.events.emit("session_expire", { topic }); } } else if (id) { @@ -1163,10 +1171,19 @@ export class Engine extends IEngine { throw new Error(message); } if (isExpired(this.client.session.get(topic).expiry)) { - await this.deleteSession(topic); + await this.deleteSession({ topic }); const { message } = getInternalError("EXPIRED", `session topic: ${topic}`); throw new Error(message); } + + if (!this.client.core.crypto.keychain.has(topic)) { + const { message } = getInternalError( + "MISSING_OR_INVALID", + `session keychain doesn't exist: ${topic}`, + ); + await this.deleteSession({ topic }); + throw new Error(message); + } } private async isValidSessionOrPairingTopic(topic: string) { diff --git a/packages/types/src/sign-client/engine.ts b/packages/types/src/sign-client/engine.ts index 1a5f0544f..32b9655cb 100644 --- a/packages/types/src/sign-client/engine.ts +++ b/packages/types/src/sign-client/engine.ts @@ -179,7 +179,12 @@ export interface EnginePrivate { onRelayEventUnknownPayload(event: EngineTypes.EventCallback): Promise; - deleteSession(topic: string, expirerHasDeleted?: boolean): Promise; + deleteSession(params: { + topic: string, + expirerHasDeleted?: boolean, + id?: number + emitEvent?: boolean + }): Promise; deleteProposal(id: number, expirerHasDeleted?: boolean): Promise; From bdd659538d90c0bc842678876408f526e4372465 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Wed, 17 Jan 2024 14:21:40 +0200 Subject: [PATCH 02/36] feat: tests --- packages/sign-client/test/sdk/client.spec.ts | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index d9074928b..22017057e 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -428,6 +428,41 @@ describe("Sign Client Integration", () => { await throttle(1000); await deleteClients(clients); }); + it("should handle invalid session state with missing keychain", async () => { + const { + clients, + sessionA: { topic }, + } = await initTwoPairedClients({}, {}, { logger: "error" }); + const dapp = clients.A as SignClient; + const sessions = dapp.session.getAll(); + expect(sessions.length).to.eq(1); + await dapp.core.crypto.keychain.del(topic); + + await Promise.all([ + new Promise((resolve) => { + dapp.on("session_delete", async (args) => { + const { topic: sessionTopic } = args; + expect(sessionTopic).to.eq(topic); + resolve(); + }); + }), + new Promise(async (resolve) => { + try { + await dapp.ping({ topic }); + } catch (err) { + expect(err.message).to.eq( + `Missing or invalid. session keychain doesn't exist: ${topic}`, + ); + } + resolve(); + }), + ]); + + const sessionsAfter = dapp.session.getAll(); + expect(sessionsAfter.length).to.eq(0); + + await deleteClients(clients); + }); }); }); }); From 6620b9c0250b3c08fe6189eb9c04f6b6dfd4ed94 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Wed, 17 Jan 2024 14:26:12 +0200 Subject: [PATCH 03/36] chore: prettier --- packages/types/src/sign-client/engine.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/types/src/sign-client/engine.ts b/packages/types/src/sign-client/engine.ts index 32b9655cb..60753206b 100644 --- a/packages/types/src/sign-client/engine.ts +++ b/packages/types/src/sign-client/engine.ts @@ -180,10 +180,10 @@ export interface EnginePrivate { onRelayEventUnknownPayload(event: EngineTypes.EventCallback): Promise; deleteSession(params: { - topic: string, - expirerHasDeleted?: boolean, - id?: number - emitEvent?: boolean + topic: string; + expirerHasDeleted?: boolean; + id?: number; + emitEvent?: boolean; }): Promise; deleteProposal(id: number, expirerHasDeleted?: boolean): Promise; From 4a73065a818f19c639b7f79fa5b4faf0a79cbf44 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Wed, 17 Jan 2024 16:44:51 +0200 Subject: [PATCH 04/36] chore: refactor messaging --- packages/sign-client/src/controllers/engine.ts | 4 ++-- packages/sign-client/test/sdk/client.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index 9e85feb26..7819be52d 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -439,7 +439,7 @@ export class Engine extends IEngine { } else { const { message } = getInternalError( "MISMATCHED_TOPIC", - `No Session or Pairing topic found: ${topic}`, + `Session or pairing topic not found: ${topic}`, ); throw new Error(message); } @@ -1179,7 +1179,7 @@ export class Engine extends IEngine { if (!this.client.core.crypto.keychain.has(topic)) { const { message } = getInternalError( "MISSING_OR_INVALID", - `session keychain doesn't exist: ${topic}`, + `session topic does not exist in keychain: ${topic}`, ); await this.deleteSession({ topic }); throw new Error(message); diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 22017057e..319f945e2 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -451,7 +451,7 @@ describe("Sign Client Integration", () => { await dapp.ping({ topic }); } catch (err) { expect(err.message).to.eq( - `Missing or invalid. session keychain doesn't exist: ${topic}`, + `Missing or invalid. session topic does not exist in keychain: ${topic}`, ); } resolve(); From 385e27a24b10c7bdd2a16fec4a069492672c2fed Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 22 Jan 2024 15:04:06 +0200 Subject: [PATCH 05/36] fix: implements synced expiries for proposals and session request --- packages/core/src/controllers/pairing.ts | 17 +++++++-- .../sign-client/src/controllers/engine.ts | 38 ++++++++++++------- packages/sign-client/test/sdk/client.spec.ts | 17 ++++----- packages/types/src/sign-client/client.ts | 1 + packages/types/src/sign-client/engine.ts | 1 + packages/types/src/sign-client/jsonrpc.ts | 2 + .../types/src/sign-client/pendingRequest.ts | 4 +- packages/types/src/sign-client/proposal.ts | 3 +- packages/utils/src/uri.ts | 4 ++ 9 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/core/src/controllers/pairing.ts b/packages/core/src/controllers/pairing.ts index abd79f3e5..68fc846f5 100644 --- a/packages/core/src/controllers/pairing.ts +++ b/packages/core/src/controllers/pairing.ts @@ -34,7 +34,7 @@ import { isJsonRpcResult, isJsonRpcError, } from "@walletconnect/jsonrpc-utils"; -import { FIVE_MINUTES, THIRTY_DAYS } from "@walletconnect/time"; +import { FIVE_MINUTES, THIRTY_DAYS, toMiliseconds } from "@walletconnect/time"; import EventEmitter from "events"; import { PAIRING_CONTEXT, @@ -99,6 +99,7 @@ export class Pairing implements IPairing { topic, symKey, relay, + expiryTimestamp: expiry, }); await this.pairings.set(topic, pairing); await this.core.relayer.subscribe(topic); @@ -110,7 +111,7 @@ export class Pairing implements IPairing { public pair: IPairing["pair"] = async (params) => { this.isInitialized(); this.isValidPair(params); - const { topic, symKey, relay } = parseUri(params.uri); + const { topic, symKey, relay, expiryTimestamp } = parseUri(params.uri); let existingPairing; if (this.pairings.keys.includes(topic)) { existingPairing = this.pairings.get(topic); @@ -121,7 +122,7 @@ export class Pairing implements IPairing { } } - const expiry = calcExpiry(FIVE_MINUTES); + const expiry = expiryTimestamp || calcExpiry(FIVE_MINUTES); const pairing = { topic, relay, expiry, active: false }; await this.pairings.set(topic, pairing); this.core.expirer.set(topic, expiry); @@ -397,6 +398,16 @@ export class Pairing implements IPairing { const { message } = getInternalError("MISSING_OR_INVALID", `pair() uri#symKey`); throw new Error(message); } + if (uri?.expiryTimestamp) { + const expiration = toMiliseconds(uri?.expiryTimestamp); + if (expiration < Date.now()) { + const { message } = getInternalError( + "EXPIRED", + `pair() URI has expired. Please try again with a new connection URI.`, + ); + throw new Error(message); + } + } }; private isValidPing = async (params: { topic: string }) => { diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index 7819be52d..8e2654e7f 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -156,6 +156,8 @@ export class Engine extends IEngine { const publicKey = await this.client.core.crypto.generateKeyPair(); + const expiry = ENGINE_RPC_OPTS.wc_sessionPropose.req.ttl || FIVE_MINUTES; + const expiryTimestamp = calcExpiry(expiry); const proposal = { requiredNamespaces, optionalNamespaces, @@ -164,13 +166,14 @@ export class Engine extends IEngine { publicKey, metadata: this.client.metadata, }, + expiryTimestamp, ...(sessionProperties && { sessionProperties }), }; const { reject, resolve, done: approval, - } = createDelayedPromise(FIVE_MINUTES, PROPOSAL_EXPIRY_MESSAGE); + } = createDelayedPromise(expiry, PROPOSAL_EXPIRY_MESSAGE); this.events.once<"session_connect">( engineEvent("session_connect"), async ({ error, session }) => { @@ -207,8 +210,7 @@ export class Engine extends IEngine { throwOnFailedPublish: true, }); - const expiry = calcExpiry(FIVE_MINUTES); - await this.setProposal(id, { id, expiry, ...proposal }); + await this.setProposal(id, { id, ...proposal }); return { uri, approval }; }; @@ -349,7 +351,7 @@ export class Engine extends IEngine { public request: IEngine["request"] = async (params: EngineTypes.RequestParams) => { await this.isInitialized(); await this.isValidRequest(params); - const { chainId, request, topic, expiry } = params; + const { chainId, request, topic, expiry = FIVE_MINUTES } = params; const id = payloadId(); const { done, resolve, reject } = createDelayedPromise( expiry, @@ -365,7 +367,7 @@ export class Engine extends IEngine { clientRpcId: id, topic, method: "wc_sessionRequest", - params: { request, chainId }, + params: { request, chainId, expiryTimestamp: calcExpiry(expiry) }, expiry, throwOnFailedPublish: true, }).catch((error) => reject(error)); @@ -541,21 +543,21 @@ export class Engine extends IEngine { private setProposal: EnginePrivate["setProposal"] = async (id, proposal) => { await this.client.proposal.set(id, proposal); - this.client.core.expirer.set(id, proposal.expiry); + this.client.core.expirer.set(id, calcExpiry(FIVE_MINUTES)); }; private setPendingSessionRequest: EnginePrivate["setPendingSessionRequest"] = async ( pendingRequest: PendingRequestTypes.Struct, ) => { - const expiry = ENGINE_RPC_OPTS.wc_sessionRequest.req.ttl; const { id, topic, params, verifyContext } = pendingRequest; + const expiry = params.expiry ? params.expiry : calcExpiry(FIVE_MINUTES); await this.client.pendingRequest.set(id, { id, topic, params, verifyContext, }); - if (expiry) this.client.core.expirer.set(id, calcExpiry(expiry)); + if (expiry) this.client.core.expirer.set(id, expiry); }; private sendRequest: EnginePrivate["sendRequest"] = async (args) => { @@ -624,7 +626,7 @@ export class Engine extends IEngine { if (toCleanup) sessionTopics.push(session.topic); }); this.client.proposal.getAll().forEach((proposal) => { - if (isExpired(proposal.expiry)) proposalIds.push(proposal.id); + if (isExpired(proposal.expiryTimestamp)) proposalIds.push(proposal.id); }); await Promise.all([ ...sessionTopics.map((topic) => this.deleteSession({ topic })), @@ -768,8 +770,8 @@ export class Engine extends IEngine { const { params, id } = payload; try { this.isValidConnect({ ...payload.params }); - const expiry = calcExpiry(FIVE_MINUTES); - const proposal = { id, pairingTopic: topic, expiry, ...params }; + const expiryTimestamp = params.expiryTimestamp || calcExpiry(FIVE_MINUTES); + const proposal = { id, pairingTopic: topic, expiryTimestamp, ...params }; await this.setProposal(id, proposal); const hash = hashMessage(JSON.stringify(payload)); const verifyContext = await this.getVerifyContext(hash, proposal.proposer.metadata); @@ -997,7 +999,17 @@ export class Engine extends IEngine { ); const session = this.client.session.get(topic); const verifyContext = await this.getVerifyContext(hash, session.peer.metadata); - const request = { id, topic, params, verifyContext }; + const expiry = params.expiryTimestamp; + delete params.expiryTimestamp; + const request = { + id, + topic, + params: { + ...params, + expiry, + }, + verifyContext, + }; await this.setPendingSessionRequest(request); this.addSessionRequestToSessionRequestQueue(request); this.processSessionRequestQueue(); @@ -1218,7 +1230,7 @@ export class Engine extends IEngine { const { message } = getInternalError("NO_MATCHING_KEY", `proposal id doesn't exist: ${id}`); throw new Error(message); } - if (isExpired(this.client.proposal.get(id).expiry)) { + if (isExpired(this.client.proposal.get(id).expiryTimestamp)) { await this.deleteProposal(id); const { message } = getInternalError("EXPIRED", `proposal id: ${id}`); throw new Error(message); diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 319f945e2..6460c1d85 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -5,7 +5,7 @@ import { JsonRpcError, } from "@walletconnect/jsonrpc-utils"; import { RelayerTypes } from "@walletconnect/types"; -import { getSdkError, parseUri } from "@walletconnect/utils"; +import { calcExpiry, getSdkError, parseUri } from "@walletconnect/utils"; import { expect, describe, it, vi } from "vitest"; import SignClient, { WALLETCONNECT_DEEPLINK_CHOICE } from "../../src"; @@ -25,6 +25,7 @@ import { initTwoPairedClients, TEST_CONNECT_PARAMS, } from "../shared"; +import { toMiliseconds } from "@walletconnect/time"; describe("Sign Client Integration", () => { it("init", async () => { @@ -552,18 +553,14 @@ describe("Sign Client Integration", () => { clients, sessionA: { topic }, } = await initTwoPairedClients({}, {}, { logger: "error" }); - const expiry = 5000; + const expiry = 600; // 10 minutes in seconds await Promise.all([ new Promise((resolve) => { - clients.A.core.relayer.once( - RELAYER_EVENTS.publish, - (payload: RelayerTypes.PublishPayload) => { - // ttl of the request should match the expiry - // expect(payload?.opts?.ttl).toEqual(expiry); - resolve(); - }, - ); + (clients.B as SignClient).once("session_request", (payload) => { + expect(payload.params.expiry).to.be.approximately(calcExpiry(expiry), 1000); + resolve(); + }); }), new Promise((resolve) => { clients.A.request({ ...TEST_REQUEST_PARAMS, topic, expiry }); diff --git a/packages/types/src/sign-client/client.ts b/packages/types/src/sign-client/client.ts index 3c907ae11..c35cf385d 100644 --- a/packages/types/src/sign-client/client.ts +++ b/packages/types/src/sign-client/client.ts @@ -39,6 +39,7 @@ export declare namespace SignClientTypes { } & BaseEventArgs<{ request: { method: string; params: any }; chainId: string; + expiry?: number; }>; session_request_sent: { request: { method: string; params: any }; diff --git a/packages/types/src/sign-client/engine.ts b/packages/types/src/sign-client/engine.ts index 60753206b..97a9f0159 100644 --- a/packages/types/src/sign-client/engine.ts +++ b/packages/types/src/sign-client/engine.ts @@ -43,6 +43,7 @@ export declare namespace EngineTypes { topic: string; symKey: string; relay: RelayerTypes.ProtocolOptions; + expiryTimestamp?: number; } interface EventCallback { diff --git a/packages/types/src/sign-client/jsonrpc.ts b/packages/types/src/sign-client/jsonrpc.ts index fa89e4fac..05902e163 100644 --- a/packages/types/src/sign-client/jsonrpc.ts +++ b/packages/types/src/sign-client/jsonrpc.ts @@ -35,6 +35,7 @@ export declare namespace JsonRpcTypes { publicKey: string; metadata: SignClientTypes.Metadata; }; + expiryTimestamp?: number; }; wc_sessionSettle: { relay: RelayerTypes.ProtocolOptions; @@ -61,6 +62,7 @@ export declare namespace JsonRpcTypes { method: string; params: any; }; + expiryTimestamp?: number; chainId: string; }; wc_sessionEvent: { diff --git a/packages/types/src/sign-client/pendingRequest.ts b/packages/types/src/sign-client/pendingRequest.ts index b4b4a5bc8..c6b82694c 100644 --- a/packages/types/src/sign-client/pendingRequest.ts +++ b/packages/types/src/sign-client/pendingRequest.ts @@ -1,11 +1,11 @@ import { IStore, Verify } from "../core"; -import { JsonRpcTypes } from "./jsonrpc"; +import { SignClientTypes } from "./"; export declare namespace PendingRequestTypes { export interface Struct { topic: string; id: number; - params: JsonRpcTypes.RequestParams["wc_sessionRequest"]; + params: SignClientTypes.EventArguments["session_request"]["params"]; verifyContext: Verify.Context; } } diff --git a/packages/types/src/sign-client/proposal.ts b/packages/types/src/sign-client/proposal.ts index be74c5294..732c59bcd 100644 --- a/packages/types/src/sign-client/proposal.ts +++ b/packages/types/src/sign-client/proposal.ts @@ -17,7 +17,8 @@ export declare namespace ProposalTypes { export interface Struct { id: number; - expiry: number; + expiry?: number; // deprecated in favour of expiryTimespamp + expiryTimestamp: number; relays: RelayerTypes.ProtocolOptions[]; proposer: { publicKey: string; diff --git a/packages/utils/src/uri.ts b/packages/utils/src/uri.ts index 23b6d27cc..98c252d9f 100644 --- a/packages/utils/src/uri.ts +++ b/packages/utils/src/uri.ts @@ -34,6 +34,9 @@ export function parseUri(str: string): EngineTypes.UriParameters { version: parseInt(requiredValues[1], 10), symKey: queryParams.symKey as string, relay: parseRelayParams(queryParams), + expiryTimestamp: queryParams.expiryTimestamp + ? parseInt(queryParams.expiryTimestamp as string, 10) + : undefined, }; return result; } @@ -60,6 +63,7 @@ export function formatUri(params: EngineTypes.UriParameters): string { qs.stringify({ symKey: params.symKey, ...formatRelayParams(params.relay), + expiryTimestamp: params.expiryTimestamp, }) ); } From 420f14ba0e44e032c540f1f83232e5cf6a2444f4 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 22 Jan 2024 15:59:29 +0200 Subject: [PATCH 06/36] fix: concurrency test --- packages/sign-client/test/sdk/client.spec.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 6460c1d85..a646257fc 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -419,12 +419,17 @@ describe("Sign Client Integration", () => { }); resolve(); }), - Array.from(Array(expectedRequests).keys()).map(() => - clients.A.request({ - topic: topicA, - ...TEST_REQUEST_PARAMS, - }), - ), + new Promise(async (resolve) => { + await Promise.all([ + ...Array.from(Array(expectedRequests).keys()).map(() => + clients.A.request({ + topic: topicA, + ...TEST_REQUEST_PARAMS, + }), + ), + ]); + resolve(); + }), ]); await throttle(1000); await deleteClients(clients); From 8a97e85dffc3540fb82d083f0e87fdf2916034f8 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 22 Jan 2024 16:14:43 +0200 Subject: [PATCH 07/36] chore: unhandled listener --- packages/sign-client/test/sdk/client.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index a646257fc..b4c938854 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -435,6 +435,9 @@ describe("Sign Client Integration", () => { await deleteClients(clients); }); it("should handle invalid session state with missing keychain", async () => { + process.on("unhandledRejection", (err) => { + console.error("ping failed", err); + }); const { clients, sessionA: { topic }, From 133673e88eb200b7aa80dd8add32805bed3e7d43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:01:00 +0100 Subject: [PATCH 08/36] chore(deps): update actions/cache action to v4 (#4156) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci_sign_client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_sign_client.yml b/.github/workflows/ci_sign_client.yml index 8928c67b6..905b7a33e 100644 --- a/.github/workflows/ci_sign_client.yml +++ b/.github/workflows/ci_sign_client.yml @@ -37,7 +37,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} From 0e7ab6d6ac32990ffca4f9862690209aa02efdbc Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 09:40:47 +0200 Subject: [PATCH 09/36] feat: tracks requests in flight before closing transport --- packages/core/src/controllers/relayer.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index 8ec37aa1f..40b56341d 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -76,6 +76,7 @@ export class Relayer extends IRelayer { private connectionStatusPollingInterval = 20; private staleConnectionErrors = ["socket hang up", "socket stalled"]; private hasExperiencedNetworkDisruption = false; + private requestsInFlight = new Map>(); constructor(opts: RelayerOptions) { super(opts); @@ -172,13 +173,19 @@ export class Relayer extends IRelayer { public request = async (request: RequestArguments) => { this.logger.debug(`Publishing Request Payload`); + const id = request.id as number; + const requestPromise = this.provider.request(request); + this.requestsInFlight.set(id, requestPromise); try { await this.toEstablishConnection(); - return await this.provider.request(request); + const result = await requestPromise; + return result; } catch (e) { this.logger.debug(`Failed to Publish Request`); this.logger.error(e as any); throw e; + } finally { + this.requestsInFlight.delete(id); } }; @@ -204,6 +211,12 @@ export class Relayer extends IRelayer { } public async transportClose() { + // wait for all requests to finish before closing the transport + if (this.requestsInFlight.size > 0) { + this.logger.warn(`waiting for requests to finish: ${this.requestsInFlight.size}`); + await Promise.all(this.requestsInFlight.values()); + } + this.transportExplicitlyClosed = true; /** * if there was a network disruption like restart of network driver, the socket is most likely stalled and we can't rely on it From 60fc1f4540a6fedff10f226ea068f2bf3f99dcfc Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 09:57:49 +0200 Subject: [PATCH 10/36] chore: logs to console --- packages/core/src/controllers/relayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index 40b56341d..80d69bad5 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -213,8 +213,8 @@ export class Relayer extends IRelayer { public async transportClose() { // wait for all requests to finish before closing the transport if (this.requestsInFlight.size > 0) { - this.logger.warn(`waiting for requests to finish: ${this.requestsInFlight.size}`); - await Promise.all(this.requestsInFlight.values()); + console.log(`waiting for requests to finish: ${this.requestsInFlight.size}`); + await Promise.all(this.requestsInFlight.values()).catch((error) => this.logger.error(error)); } this.transportExplicitlyClosed = true; From b7757d590ec465b95c46fa6b27ed03308f08b199 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 10:01:44 +0200 Subject: [PATCH 11/36] chore: lint --- packages/core/src/controllers/relayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index 80d69bad5..45c7e17fa 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -213,6 +213,7 @@ export class Relayer extends IRelayer { public async transportClose() { // wait for all requests to finish before closing the transport if (this.requestsInFlight.size > 0) { + // eslint-disable-next-line no-console console.log(`waiting for requests to finish: ${this.requestsInFlight.size}`); await Promise.all(this.requestsInFlight.values()).catch((error) => this.logger.error(error)); } From 28b62ecd3e307d137751e20d4643d0f4b888a28b Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 10:29:38 +0200 Subject: [PATCH 12/36] chore: log awaited request --- packages/core/src/controllers/relayer.ts | 26 +++++++++++++++++++----- packages/core/src/core.ts | 1 + packages/sign-client/test/shared/init.ts | 8 +++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index 45c7e17fa..c32ae3af6 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { EventEmitter } from "events"; import { JsonRpcProvider } from "@walletconnect/jsonrpc-provider"; import { @@ -76,7 +77,13 @@ export class Relayer extends IRelayer { private connectionStatusPollingInterval = 20; private staleConnectionErrors = ["socket hang up", "socket stalled"]; private hasExperiencedNetworkDisruption = false; - private requestsInFlight = new Map>(); + private requestsInFlight = new Map< + number, + { + promise: Promise; + request: RequestArguments; + } + >(); constructor(opts: RelayerOptions) { super(opts); @@ -175,7 +182,10 @@ export class Relayer extends IRelayer { this.logger.debug(`Publishing Request Payload`); const id = request.id as number; const requestPromise = this.provider.request(request); - this.requestsInFlight.set(id, requestPromise); + this.requestsInFlight.set(id, { + promise: requestPromise, + request, + }); try { await this.toEstablishConnection(); const result = await requestPromise; @@ -213,9 +223,15 @@ export class Relayer extends IRelayer { public async transportClose() { // wait for all requests to finish before closing the transport if (this.requestsInFlight.size > 0) { - // eslint-disable-next-line no-console - console.log(`waiting for requests to finish: ${this.requestsInFlight.size}`); - await Promise.all(this.requestsInFlight.values()).catch((error) => this.logger.error(error)); + const identifier = Math.random().toString(36).substring(7); + console.log( + `${identifier} | ${this.core.name} - waiting for requests to finish: ${this.requestsInFlight.size}`, + ); + this.requestsInFlight.forEach(async (value) => { + console.log(`${identifier} | ${this.core.name} - waiting for request`, value.request); + await value.promise; + }); + console.log(`${identifier} | ${this.core.name} - requests finished`); } this.transportExplicitlyClosed = true; diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index fc605fdf8..ab515b0ee 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -87,6 +87,7 @@ export class Core extends ICore { this.pairing = new Pairing(this, this.logger); this.verify = new Verify(this.projectId || "", this.logger); this.echoClient = new EchoClient(this.projectId || "", this.logger); + this.name = opts?.name || CORE_DEFAULT.name; } get context() { diff --git a/packages/sign-client/test/shared/init.ts b/packages/sign-client/test/shared/init.ts index aef939201..2a5e7b9c9 100644 --- a/packages/sign-client/test/shared/init.ts +++ b/packages/sign-client/test/shared/init.ts @@ -22,12 +22,14 @@ export async function initTwoClients( sharedClientOpts: SignClientTypes.Options = {}, ) { const A = await SignClient.init({ + name: "A", ...TEST_SIGN_CLIENT_OPTIONS_A, ...sharedClientOpts, ...clientOptsA, }); const B = await SignClient.init({ + name: "B", ...TEST_SIGN_CLIENT_OPTIONS_B, ...sharedClientOpts, ...clientOptsB, @@ -41,7 +43,7 @@ export async function initTwoPairedClients( clientOptsB: SignClientTypes.Options = {}, sharedClientOpts: SignClientTypes.Options = {}, ) { - let clients; + let clients: Clients; let pairingA; let sessionA; let retries = 0; @@ -50,10 +52,10 @@ export async function initTwoPairedClients( throw new Error("Could not pair clients"); } try { - clients = await createExpiringPromise( + clients = (await createExpiringPromise( initTwoClients(clientOptsA, clientOptsB, sharedClientOpts), TESTS_CONNECT_TIMEOUT, - ); + )) as Clients; const settled: any = await createExpiringPromise( testConnectMethod(clients), From cf5c140aecc1702c547ada0cce5a3ad5ecd04af6 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 10:40:52 +0200 Subject: [PATCH 13/36] chore: logs failed publish id --- packages/core/src/controllers/publisher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/controllers/publisher.ts b/packages/core/src/controllers/publisher.ts index c770bc9ad..df7740ec2 100644 --- a/packages/core/src/controllers/publisher.ts +++ b/packages/core/src/controllers/publisher.ts @@ -50,7 +50,7 @@ export class Publisher extends IPublisher { const publish = await createExpiringPromise( this.rpcPublish(topic, message, ttl, relay, prompt, tag, id), this.publishTimeout, - "Failed to publish payload, please try again.", + `Failed to publish payload, please try again. id:${id} tag:${tag}`, ); await publish; this.removeRequestFromQueue(id); From 2358bd343dc7cfc01a1a74b83452ce7e34255428 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 11:11:40 +0200 Subject: [PATCH 14/36] refactor: awaits request --- packages/sign-client/test/sdk/client.spec.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index b4c938854..5d031a59f 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -337,11 +337,12 @@ describe("Sign Client Integration", () => { if (receivedRequests >= expectedRequests) resolve(); }); }), - Array.from(Array(expectedRequests).keys()).map(() => - clients.A.request({ - topic, - ...TEST_REQUEST_PARAMS, - }), + Array.from(Array(expectedRequests).keys()).map( + async () => + await clients.A.request({ + topic, + ...TEST_REQUEST_PARAMS, + }), ), ]); await throttle(1000); From 098e36bb248627a2c3ecdf766f0239c52e484779 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 11:20:43 +0200 Subject: [PATCH 15/36] fix: awaits all requests in tests --- packages/sign-client/test/sdk/client.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 5d031a59f..d4b01af37 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -413,8 +413,8 @@ describe("Sign Client Integration", () => { if (receivedRequests >= expectedRequests) resolve(); }); }), - new Promise((resolve) => { - clients.A.request({ + new Promise(async (resolve) => { + await clients.A.request({ topic: topicB, ...TEST_REQUEST_PARAMS, }); @@ -447,7 +447,6 @@ describe("Sign Client Integration", () => { const sessions = dapp.session.getAll(); expect(sessions.length).to.eq(1); await dapp.core.crypto.keychain.del(topic); - await Promise.all([ new Promise((resolve) => { dapp.on("session_delete", async (args) => { @@ -571,8 +570,8 @@ describe("Sign Client Integration", () => { resolve(); }); }), - new Promise((resolve) => { - clients.A.request({ ...TEST_REQUEST_PARAMS, topic, expiry }); + new Promise(async (resolve) => { + await clients.A.request({ ...TEST_REQUEST_PARAMS, topic, expiry }); resolve(); }), ]); @@ -629,8 +628,8 @@ describe("Sign Client Integration", () => { resolve(); }); }), - new Promise((resolve) => { - clients.A.request({ ...testRequestProps, topic }); + new Promise(async (resolve) => { + await clients.A.request({ ...testRequestProps, topic }); resolve(); }), ]); From 0f4a2b8cc3310a7c9974e3b99e122115de230d0e Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 12:30:24 +0200 Subject: [PATCH 16/36] refactor: session queue test --- .../sign-client/src/controllers/engine.ts | 26 +++++++++++++++++ packages/sign-client/test/sdk/client.spec.ts | 28 ++++++++----------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index 8e2654e7f..b062618f4 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -16,6 +16,7 @@ import { isJsonRpcResponse, isJsonRpcResult, JsonRpcRequest, + ErrorResponse, } from "@walletconnect/jsonrpc-utils"; import { FIVE_MINUTES, ONE_SECOND, toMiliseconds } from "@walletconnect/time"; import { @@ -984,6 +985,7 @@ export class Engine extends IEngine { }); }), this.sendResult<"wc_sessionDelete">({ id, topic, result: true }), + this.cleanupPendingSentRequestsForTopic({ topic, error: getSdkError("USER_DISCONNECTED") }), ]); } catch (err: any) { this.client.logger.error(err); @@ -1070,6 +1072,30 @@ export class Engine extends IEngine { }, toMiliseconds(this.requestQueueDelay)); }; + // Allows for cleanup on any sent pending requests if the peer disconnects the session before responding + private cleanupPendingSentRequestsForTopic = ({ + topic, + error, + }: { + topic: string; + error: ErrorResponse; + }) => { + const pendingRequests = this.client.core.history.pending; + if (pendingRequests.length > 0) { + const forSession = pendingRequests.filter( + (r) => r.topic === topic && r.request.method === "wc_sessionRequest", + ); + if (forSession.length > 0) { + forSession.forEach((r) => { + // notify .request() handler of the rejection + this.events.emit(engineEvent("session_request", r.request.id), { + error, + }); + }); + } + } + }; + private processSessionRequestQueue = () => { if (this.sessionRequestQueue.state === ENGINE_QUEUE_STATES.active) { this.client.logger.info("session request queue is already active."); diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index d4b01af37..4e9abb657 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -410,24 +410,23 @@ describe("Sign Client Integration", () => { // eslint-disable-next-line no-console console.log("respond error", err); }); - if (receivedRequests >= expectedRequests) resolve(); - }); - }), - new Promise(async (resolve) => { - await clients.A.request({ - topic: topicB, - ...TEST_REQUEST_PARAMS, + if (receivedRequests > expectedRequests) resolve(); }); - resolve(); }), new Promise(async (resolve) => { await Promise.all([ - ...Array.from(Array(expectedRequests).keys()).map(() => - clients.A.request({ - topic: topicA, - ...TEST_REQUEST_PARAMS, - }), + ...Array.from(Array(expectedRequests).keys()).map( + async () => + await clients.A.request({ + topic: topicA, + ...TEST_REQUEST_PARAMS, + }), ), + clients.A.request({ + topic: topicB, + ...TEST_REQUEST_PARAMS, + // eslint-disable-next-line no-console + }).catch((e) => console.error(e)), // capture the error from the session disconnect ]); resolve(); }), @@ -436,9 +435,6 @@ describe("Sign Client Integration", () => { await deleteClients(clients); }); it("should handle invalid session state with missing keychain", async () => { - process.on("unhandledRejection", (err) => { - console.error("ping failed", err); - }); const { clients, sessionA: { topic }, From cdabfd080a2ccaa6c7537f32aaaf9b9c08c8f79e Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 12:58:41 +0200 Subject: [PATCH 17/36] chore: skips queue test --- packages/sign-client/test/sdk/client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 4e9abb657..595ed7dab 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -353,7 +353,7 @@ describe("Sign Client Integration", () => { * while session request is being approved * the queue should continue operating normally after the `respond` rejection */ - it("continue processing requests queue after respond rejection due to disconnected session", async () => { + it.skip("continue processing requests queue after respond rejection due to disconnected session", async () => { // create the clients and pair them const { clients, From f71137b79bf61ce012a6a790b7a70dd1bc83d386 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 14:23:11 +0200 Subject: [PATCH 18/36] fix: responds to requests in tests --- packages/sign-client/test/sdk/client.spec.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 595ed7dab..4cb8d5cbd 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -25,7 +25,6 @@ import { initTwoPairedClients, TEST_CONNECT_PARAMS, } from "../shared"; -import { toMiliseconds } from "@walletconnect/time"; describe("Sign Client Integration", () => { it("init", async () => { @@ -561,8 +560,12 @@ describe("Sign Client Integration", () => { await Promise.all([ new Promise((resolve) => { - (clients.B as SignClient).once("session_request", (payload) => { + (clients.B as SignClient).once("session_request", async (payload) => { expect(payload.params.expiry).to.be.approximately(calcExpiry(expiry), 1000); + await clients.B.respond({ + topic, + response: formatJsonRpcResult(payload.id, "test response"), + }); resolve(); }); }), @@ -611,7 +614,7 @@ describe("Sign Client Integration", () => { }; await Promise.all([ new Promise((resolve) => { - clients.B.once("session_request", (payload) => { + clients.B.once("session_request", async (payload) => { const { params } = payload; const session = clients.B.session.get(payload.topic); expect(params).toMatchObject(testRequestProps); @@ -621,6 +624,10 @@ describe("Sign Client Integration", () => { ), ).to.exist; expect(session.requiredNamespaces[TEST_AVALANCHE_CHAIN]).to.exist; + await clients.B.respond({ + topic, + response: formatJsonRpcResult(payload.id, "test response"), + }); resolve(); }); }), From 4c72ae6bafd46e4dd772bb5055a26d3e3f1d6aca Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 14:59:28 +0200 Subject: [PATCH 19/36] chore: increase publish timeout --- packages/core/src/controllers/publisher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/controllers/publisher.ts b/packages/core/src/controllers/publisher.ts index df7740ec2..2dc14db0c 100644 --- a/packages/core/src/controllers/publisher.ts +++ b/packages/core/src/controllers/publisher.ts @@ -20,7 +20,7 @@ export class Publisher extends IPublisher { public name = PUBLISHER_CONTEXT; public queue = new Map(); - private publishTimeout = toMiliseconds(TEN_SECONDS); + private publishTimeout = toMiliseconds(TEN_SECONDS + 5); private needsTransportRestart = false; constructor(public relayer: IRelayer, public logger: Logger) { From 57d6606b638ab9d3f01064523164058728a7a5bf Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 15:28:43 +0200 Subject: [PATCH 20/36] refactor: increase publish timeout --- packages/core/src/controllers/publisher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/controllers/publisher.ts b/packages/core/src/controllers/publisher.ts index 2dc14db0c..c584be1e1 100644 --- a/packages/core/src/controllers/publisher.ts +++ b/packages/core/src/controllers/publisher.ts @@ -20,7 +20,7 @@ export class Publisher extends IPublisher { public name = PUBLISHER_CONTEXT; public queue = new Map(); - private publishTimeout = toMiliseconds(TEN_SECONDS + 5); + private publishTimeout = toMiliseconds(TEN_SECONDS + TEN_SECONDS); private needsTransportRestart = false; constructor(public relayer: IRelayer, public logger: Logger) { From d5b75120bb171870344a01ddf36af535e3d50163 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 15:28:54 +0200 Subject: [PATCH 21/36] refactor: cleanup logs --- packages/core/src/controllers/relayer.ts | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index c32ae3af6..dd08f4da5 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -77,13 +77,7 @@ export class Relayer extends IRelayer { private connectionStatusPollingInterval = 20; private staleConnectionErrors = ["socket hang up", "socket stalled"]; private hasExperiencedNetworkDisruption = false; - private requestsInFlight = new Map< - number, - { - promise: Promise; - request: RequestArguments; - } - >(); + private requestsInFlight = new Map>(); constructor(opts: RelayerOptions) { super(opts); @@ -182,10 +176,7 @@ export class Relayer extends IRelayer { this.logger.debug(`Publishing Request Payload`); const id = request.id as number; const requestPromise = this.provider.request(request); - this.requestsInFlight.set(id, { - promise: requestPromise, - request, - }); + this.requestsInFlight.set(id, requestPromise); try { await this.toEstablishConnection(); const result = await requestPromise; @@ -223,15 +214,7 @@ export class Relayer extends IRelayer { public async transportClose() { // wait for all requests to finish before closing the transport if (this.requestsInFlight.size > 0) { - const identifier = Math.random().toString(36).substring(7); - console.log( - `${identifier} | ${this.core.name} - waiting for requests to finish: ${this.requestsInFlight.size}`, - ); - this.requestsInFlight.forEach(async (value) => { - console.log(`${identifier} | ${this.core.name} - waiting for request`, value.request); - await value.promise; - }); - console.log(`${identifier} | ${this.core.name} - requests finished`); + await Promise.all(this.requestsInFlight.values()); } this.transportExplicitlyClosed = true; From f492a1c331776204c9b659890960b37272c03e53 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 16:08:38 +0200 Subject: [PATCH 22/36] refactor: request in flight await --- packages/core/src/controllers/relayer.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index dd08f4da5..377f47e8c 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -77,7 +77,13 @@ export class Relayer extends IRelayer { private connectionStatusPollingInterval = 20; private staleConnectionErrors = ["socket hang up", "socket stalled"]; private hasExperiencedNetworkDisruption = false; - private requestsInFlight = new Map>(); + private requestsInFlight = new Map< + number, + { + promise: Promise; + request: RequestArguments; + } + >(); constructor(opts: RelayerOptions) { super(opts); @@ -176,7 +182,10 @@ export class Relayer extends IRelayer { this.logger.debug(`Publishing Request Payload`); const id = request.id as number; const requestPromise = this.provider.request(request); - this.requestsInFlight.set(id, requestPromise); + this.requestsInFlight.set(id, { + promise: requestPromise, + request, + }); try { await this.toEstablishConnection(); const result = await requestPromise; @@ -214,7 +223,9 @@ export class Relayer extends IRelayer { public async transportClose() { // wait for all requests to finish before closing the transport if (this.requestsInFlight.size > 0) { - await Promise.all(this.requestsInFlight.values()); + this.requestsInFlight.forEach(async (value) => { + await value.promise; + }); } this.transportExplicitlyClosed = true; From 55f64c97b83c3329cebf94f91fa74e70e859c279 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 16:15:38 +0200 Subject: [PATCH 23/36] chore: enable queue test --- packages/sign-client/test/sdk/client.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 4cb8d5cbd..3dd146761 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -165,7 +165,6 @@ describe("Sign Client Integration", () => { // 7. attempt to pair again with the same URI // 8. should receive an error the pairing already exists await expect(wallet.pair({ uri })).rejects.toThrowError(); - await deleteClients({ A: dapp, B: wallet }); }); }); @@ -352,7 +351,7 @@ describe("Sign Client Integration", () => { * while session request is being approved * the queue should continue operating normally after the `respond` rejection */ - it.skip("continue processing requests queue after respond rejection due to disconnected session", async () => { + it("continue processing requests queue after respond rejection due to disconnected session", async () => { // create the clients and pair them const { clients, From 4181b6a51db40447210f89dba764b9d43d2209f8 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 16:20:47 +0200 Subject: [PATCH 24/36] chore: cleanup --- packages/core/src/controllers/relayer.ts | 1 - packages/core/src/core.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index 377f47e8c..ecbf0afd2 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { EventEmitter } from "events"; import { JsonRpcProvider } from "@walletconnect/jsonrpc-provider"; import { diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index ab515b0ee..fc605fdf8 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -87,7 +87,6 @@ export class Core extends ICore { this.pairing = new Pairing(this, this.logger); this.verify = new Verify(this.projectId || "", this.logger); this.echoClient = new EchoClient(this.projectId || "", this.logger); - this.name = opts?.name || CORE_DEFAULT.name; } get context() { From 6ef8fa6ec55a41b8d4a37ea7b2f50aa256a2d44b Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 16:30:02 +0200 Subject: [PATCH 25/36] refactor: moves `expiryTimestamp` inside `request` --- packages/sign-client/src/controllers/engine.ts | 17 +++++++++-------- packages/sign-client/test/sdk/client.spec.ts | 5 ++++- packages/types/src/sign-client/client.ts | 3 +-- packages/types/src/sign-client/jsonrpc.ts | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index b062618f4..a9c58ae99 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -368,7 +368,13 @@ export class Engine extends IEngine { clientRpcId: id, topic, method: "wc_sessionRequest", - params: { request, chainId, expiryTimestamp: calcExpiry(expiry) }, + params: { + request: { + ...request, + expiryTimestamp: calcExpiry(expiry), + }, + chainId, + }, expiry, throwOnFailedPublish: true, }).catch((error) => reject(error)); @@ -551,7 +557,7 @@ export class Engine extends IEngine { pendingRequest: PendingRequestTypes.Struct, ) => { const { id, topic, params, verifyContext } = pendingRequest; - const expiry = params.expiry ? params.expiry : calcExpiry(FIVE_MINUTES); + const expiry = params.request.expiryTimestamp || calcExpiry(FIVE_MINUTES); await this.client.pendingRequest.set(id, { id, topic, @@ -1001,15 +1007,10 @@ export class Engine extends IEngine { ); const session = this.client.session.get(topic); const verifyContext = await this.getVerifyContext(hash, session.peer.metadata); - const expiry = params.expiryTimestamp; - delete params.expiryTimestamp; const request = { id, topic, - params: { - ...params, - expiry, - }, + params, verifyContext, }; await this.setPendingSessionRequest(request); diff --git a/packages/sign-client/test/sdk/client.spec.ts b/packages/sign-client/test/sdk/client.spec.ts index 3dd146761..5343274f2 100644 --- a/packages/sign-client/test/sdk/client.spec.ts +++ b/packages/sign-client/test/sdk/client.spec.ts @@ -560,7 +560,10 @@ describe("Sign Client Integration", () => { await Promise.all([ new Promise((resolve) => { (clients.B as SignClient).once("session_request", async (payload) => { - expect(payload.params.expiry).to.be.approximately(calcExpiry(expiry), 1000); + expect(payload.params.request.expiryTimestamp).to.be.approximately( + calcExpiry(expiry), + 1000, + ); await clients.B.respond({ topic, response: formatJsonRpcResult(payload.id, "test response"), diff --git a/packages/types/src/sign-client/client.ts b/packages/types/src/sign-client/client.ts index c35cf385d..79d253fe8 100644 --- a/packages/types/src/sign-client/client.ts +++ b/packages/types/src/sign-client/client.ts @@ -37,9 +37,8 @@ export declare namespace SignClientTypes { session_request: { verifyContext: Verify.Context; } & BaseEventArgs<{ - request: { method: string; params: any }; + request: { method: string; params: any; expiryTimestamp?: number; }; chainId: string; - expiry?: number; }>; session_request_sent: { request: { method: string; params: any }; diff --git a/packages/types/src/sign-client/jsonrpc.ts b/packages/types/src/sign-client/jsonrpc.ts index 05902e163..8e0097fa6 100644 --- a/packages/types/src/sign-client/jsonrpc.ts +++ b/packages/types/src/sign-client/jsonrpc.ts @@ -61,8 +61,8 @@ export declare namespace JsonRpcTypes { request: { method: string; params: any; + expiryTimestamp?: number; }; - expiryTimestamp?: number; chainId: string; }; wc_sessionEvent: { From d6170dce19c25aebe421a9239d6a4c54344bed9d Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Thu, 25 Jan 2024 16:35:32 +0200 Subject: [PATCH 26/36] chore: prettier --- packages/types/src/sign-client/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/sign-client/client.ts b/packages/types/src/sign-client/client.ts index 79d253fe8..bcfc8d8f0 100644 --- a/packages/types/src/sign-client/client.ts +++ b/packages/types/src/sign-client/client.ts @@ -37,7 +37,7 @@ export declare namespace SignClientTypes { session_request: { verifyContext: Verify.Context; } & BaseEventArgs<{ - request: { method: string; params: any; expiryTimestamp?: number; }; + request: { method: string; params: any; expiryTimestamp?: number }; chainId: string; }>; session_request_sent: { From 75a77612eb364990c4f2f0176432133955cc21d9 Mon Sep 17 00:00:00 2001 From: Ivan Reshetnikov Date: Fri, 26 Jan 2024 08:29:54 +0100 Subject: [PATCH 27/36] feat: cloudwatch metrics tags (#4172) --- packages/sign-client/test/shared/metrics.ts | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/sign-client/test/shared/metrics.ts b/packages/sign-client/test/shared/metrics.ts index fd222faaa..c8d0355a4 100644 --- a/packages/sign-client/test/shared/metrics.ts +++ b/packages/sign-client/test/shared/metrics.ts @@ -1,5 +1,7 @@ import { CloudWatch, PutMetricDataCommandInput } from "@aws-sdk/client-cloudwatch"; +const tag = process.env.TAG || "default"; + export const uploadCanaryResultsToCloudWatch = async ( env: string, region: string, @@ -23,6 +25,10 @@ export const uploadCanaryResultsToCloudWatch = async ( Name: "Region", Value: region, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Count", Value: isTestPassed ? 1 : 0, @@ -39,6 +45,10 @@ export const uploadCanaryResultsToCloudWatch = async ( Name: "Region", Value: region, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Count", Value: isTestPassed ? 0 : 1, @@ -58,6 +68,10 @@ export const uploadCanaryResultsToCloudWatch = async ( Name: "Region", Value: region, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Milliseconds", Value: testDurationMs, @@ -78,6 +92,10 @@ export const uploadCanaryResultsToCloudWatch = async ( Name: "Region", Value: region, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Milliseconds", Value: metric[metricName], @@ -124,6 +142,10 @@ export const uploadLoadTestConnectionDataToCloudWatch = async ( Name: "Target", Value: target, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Count", Value: successfullyConnected, @@ -136,6 +158,10 @@ export const uploadLoadTestConnectionDataToCloudWatch = async ( Name: "Target", Value: target, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Count", Value: failedToConnect, @@ -148,6 +174,10 @@ export const uploadLoadTestConnectionDataToCloudWatch = async ( Name: "Target", Value: target, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Milliseconds", Value: averagePairingTimeMs, @@ -160,6 +190,10 @@ export const uploadLoadTestConnectionDataToCloudWatch = async ( Name: "Target", Value: target, }, + { + Name: "Tag", + Value: tag, + }, ], Unit: "Milliseconds", Value: averageHandshakeTimeMs, From 372c355bd8e4e56812b7ea0e1954aff34f02462a Mon Sep 17 00:00:00 2001 From: Derek Date: Fri, 26 Jan 2024 07:02:29 -0500 Subject: [PATCH 28/36] feat(canary): remove artificial delay (#4174) --- packages/sign-client/test/canary/canary.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sign-client/test/canary/canary.spec.ts b/packages/sign-client/test/canary/canary.spec.ts index 91958b1ee..63ffbfdd7 100644 --- a/packages/sign-client/test/canary/canary.spec.ts +++ b/packages/sign-client/test/canary/canary.spec.ts @@ -4,7 +4,6 @@ import { testConnectMethod, deleteClients, uploadCanaryResultsToCloudWatch, - throttle, publishToStatusPage, } from "../shared"; import { @@ -53,7 +52,6 @@ describe("Canary", () => { const pairingLatencyMs = Date.now() - start - humanInputLatencyMs; // Send a ping - await throttle(humanInputLatencyMs); // Introduce some realistic timeout and allow backend to replicate const pingStart = Date.now(); await new Promise(async (resolve, reject) => { try { From e1d315253ff5979b6852db19898ec1576973689b Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Fri, 26 Jan 2024 16:15:53 +0200 Subject: [PATCH 29/36] feat: adds `session_request_expire` & `proposal_expire` to w3w --- packages/sign-client/src/constants/client.ts | 1 + packages/sign-client/src/controllers/engine.ts | 1 + packages/types/src/sign-client/client.ts | 4 +++- packages/web3wallet/src/controllers/engine.ts | 10 ++++++++++ packages/web3wallet/src/types/client.ts | 14 +++++++++++++- 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/sign-client/src/constants/client.ts b/packages/sign-client/src/constants/client.ts index e8777e772..0b76fa598 100644 --- a/packages/sign-client/src/constants/client.ts +++ b/packages/sign-client/src/constants/client.ts @@ -24,6 +24,7 @@ export const SIGN_CLIENT_EVENTS: Record { id: number; @@ -51,6 +52,7 @@ export declare namespace SignClientTypes { chainId: string; }>; proposal_expire: { id: number }; + session_request_expire: { id: number }; } type Metadata = CoreTypes.Metadata; diff --git a/packages/web3wallet/src/controllers/engine.ts b/packages/web3wallet/src/controllers/engine.ts index cbcbdcfbd..7638bbaf8 100644 --- a/packages/web3wallet/src/controllers/engine.ts +++ b/packages/web3wallet/src/controllers/engine.ts @@ -121,10 +121,20 @@ export class Engine extends IWeb3WalletEngine { this.client.events.emit("auth_request", event); }; + private onProposalExpire = (event: Web3WalletTypes.ProposalExpire) => { + this.client.events.emit("proposal_expire", event); + }; + + private onSessionRequestExpire = (event: Web3WalletTypes.SessionRequestExpire) => { + this.client.events.emit("session_request_expire", event); + }; + private initializeEventListeners = () => { this.signClient.events.on("session_proposal", this.onSessionProposal); this.signClient.events.on("session_request", this.onSessionRequest); this.signClient.events.on("session_delete", this.onSessionDelete); this.authClient.on("auth_request", this.onAuthRequest); + this.signClient.events.on("proposal_expire", this.onProposalExpire); + this.signClient.events.on("session_request_expire", this.onSessionRequestExpire); }; } diff --git a/packages/web3wallet/src/types/client.ts b/packages/web3wallet/src/types/client.ts index 48865f250..823b36dbb 100644 --- a/packages/web3wallet/src/types/client.ts +++ b/packages/web3wallet/src/types/client.ts @@ -6,7 +6,13 @@ import { Logger } from "@walletconnect/logger"; import { JsonRpcPayload } from "@walletconnect/jsonrpc-utils"; export declare namespace Web3WalletTypes { - type Event = "session_proposal" | "session_request" | "session_delete" | "auth_request"; + type Event = + | "session_proposal" + | "session_request" + | "session_delete" + | "auth_request" + | "proposal_expire" + | "session_request_expire"; interface BaseEventArgs { id: number; @@ -29,11 +35,17 @@ export declare namespace Web3WalletTypes { type SessionDelete = Omit; + type ProposalExpire = { id: number }; + + type SessionRequestExpire = { id: number }; + interface EventArguments { session_proposal: SessionProposal; session_request: SessionRequest; session_delete: Omit; auth_request: AuthRequest; + proposal_expire: ProposalExpire; + session_request_expire: SessionRequestExpire; } interface Options { From 23da06de63f9f2c49bcfbb2e4161ead96b1c9424 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Fri, 26 Jan 2024 16:36:27 +0200 Subject: [PATCH 30/36] refactor: implementation as per review --- packages/core/src/controllers/publisher.ts | 2 +- packages/core/src/controllers/relayer.ts | 1 + .../sign-client/src/controllers/engine.ts | 22 +++++++++---------- packages/types/src/sign-client/engine.ts | 8 +++++-- packages/types/src/sign-client/proposal.ts | 5 ++++- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/core/src/controllers/publisher.ts b/packages/core/src/controllers/publisher.ts index c584be1e1..722e6ede2 100644 --- a/packages/core/src/controllers/publisher.ts +++ b/packages/core/src/controllers/publisher.ts @@ -20,7 +20,7 @@ export class Publisher extends IPublisher { public name = PUBLISHER_CONTEXT; public queue = new Map(); - private publishTimeout = toMiliseconds(TEN_SECONDS + TEN_SECONDS); + private publishTimeout = toMiliseconds(TEN_SECONDS * 2); private needsTransportRestart = false; constructor(public relayer: IRelayer, public logger: Logger) { diff --git a/packages/core/src/controllers/relayer.ts b/packages/core/src/controllers/relayer.ts index ecbf0afd2..88b2ddd2b 100644 --- a/packages/core/src/controllers/relayer.ts +++ b/packages/core/src/controllers/relayer.ts @@ -222,6 +222,7 @@ export class Relayer extends IRelayer { public async transportClose() { // wait for all requests to finish before closing the transport if (this.requestsInFlight.size > 0) { + this.logger.debug("Waiting for all in-flight requests to finish before closing transport..."); this.requestsInFlight.forEach(async (value) => { await value.promise; }); diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index a9c58ae99..472f15cb7 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -352,7 +352,7 @@ export class Engine extends IEngine { public request: IEngine["request"] = async (params: EngineTypes.RequestParams) => { await this.isInitialized(); await this.isValidRequest(params); - const { chainId, request, topic, expiry = FIVE_MINUTES } = params; + const { chainId, request, topic, expiry = ENGINE_RPC_OPTS.wc_sessionRequest.req.ttl } = params; const id = payloadId(); const { done, resolve, reject } = createDelayedPromise( expiry, @@ -550,14 +550,15 @@ export class Engine extends IEngine { private setProposal: EnginePrivate["setProposal"] = async (id, proposal) => { await this.client.proposal.set(id, proposal); - this.client.core.expirer.set(id, calcExpiry(FIVE_MINUTES)); + this.client.core.expirer.set(id, calcExpiry(ENGINE_RPC_OPTS.wc_sessionPropose.req.ttl)); }; private setPendingSessionRequest: EnginePrivate["setPendingSessionRequest"] = async ( pendingRequest: PendingRequestTypes.Struct, ) => { const { id, topic, params, verifyContext } = pendingRequest; - const expiry = params.request.expiryTimestamp || calcExpiry(FIVE_MINUTES); + const expiry = + params.request.expiryTimestamp || calcExpiry(ENGINE_RPC_OPTS.wc_sessionRequest.req.ttl); await this.client.pendingRequest.set(id, { id, topic, @@ -777,7 +778,8 @@ export class Engine extends IEngine { const { params, id } = payload; try { this.isValidConnect({ ...payload.params }); - const expiryTimestamp = params.expiryTimestamp || calcExpiry(FIVE_MINUTES); + const expiryTimestamp = + params.expiryTimestamp || calcExpiry(ENGINE_RPC_OPTS.wc_sessionPropose.req.ttl); const proposal = { id, pairingTopic: topic, expiryTimestamp, ...params }; await this.setProposal(id, proposal); const hash = hashMessage(JSON.stringify(payload)); @@ -1086,14 +1088,12 @@ export class Engine extends IEngine { const forSession = pendingRequests.filter( (r) => r.topic === topic && r.request.method === "wc_sessionRequest", ); - if (forSession.length > 0) { - forSession.forEach((r) => { - // notify .request() handler of the rejection - this.events.emit(engineEvent("session_request", r.request.id), { - error, - }); + forSession.forEach((r) => { + // notify .request() handler of the rejection + this.events.emit(engineEvent("session_request", r.request.id), { + error, }); - } + }); } }; diff --git a/packages/types/src/sign-client/engine.ts b/packages/types/src/sign-client/engine.ts index 97a9f0159..57f9a816f 100644 --- a/packages/types/src/sign-client/engine.ts +++ b/packages/types/src/sign-client/engine.ts @@ -124,8 +124,12 @@ export declare namespace EngineTypes { type AcknowledgedPromise = Promise<{ acknowledged: () => Promise }>; interface RpcOpts { - req: RelayerTypes.PublishOptions; - res: RelayerTypes.PublishOptions; + req: RelayerTypes.PublishOptions & { + ttl: number; + }; + res: RelayerTypes.PublishOptions & { + ttl: number; + }; } type RpcOptsMap = Record; diff --git a/packages/types/src/sign-client/proposal.ts b/packages/types/src/sign-client/proposal.ts index 732c59bcd..f5c8910af 100644 --- a/packages/types/src/sign-client/proposal.ts +++ b/packages/types/src/sign-client/proposal.ts @@ -17,7 +17,10 @@ export declare namespace ProposalTypes { export interface Struct { id: number; - expiry?: number; // deprecated in favour of expiryTimespamp + /** + * @deprecated in favor of expiryTimestamp + */ + expiry?: number; expiryTimestamp: number; relays: RelayerTypes.ProtocolOptions[]; proposer: { From 8426d5c59e125406733f40cb32de3e59398cf511 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Fri, 26 Jan 2024 17:18:10 +0200 Subject: [PATCH 31/36] refactor: skip disconnect step --- providers/universal-provider/test/index.spec.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/providers/universal-provider/test/index.spec.ts b/providers/universal-provider/test/index.spec.ts index 2229fa12a..57927bcc3 100644 --- a/providers/universal-provider/test/index.spec.ts +++ b/providers/universal-provider/test/index.spec.ts @@ -66,22 +66,7 @@ describe("UniversalProvider", function () { afterAll(async () => { // close test network await testNetwork.close(); - // disconnect provider - await Promise.all([ - new Promise((resolve) => { - provider.on("session_delete", () => { - resolve(); - }); - }), - new Promise(async (resolve) => { - await walletClient.disconnect(); - resolve(); - }), - ]); - expect(walletClient.client?.session.values.length).to.eql(0); - await throttle(1_000); - await provider.client.core.relayer.transportClose(); - await walletClient.client?.core.relayer.transportClose(); + await deleteProviders({ A: provider, B: walletClient.provider }); }); describe("eip155", () => { From b3ae111a2fb277c36aea033abaad3baba224dcbf Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 29 Jan 2024 12:03:06 +0200 Subject: [PATCH 32/36] feat: adds tests --- packages/web3wallet/test/sign.spec.ts | 105 +++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/web3wallet/test/sign.spec.ts b/packages/web3wallet/test/sign.spec.ts index 8194e388c..5723b5917 100644 --- a/packages/web3wallet/test/sign.spec.ts +++ b/packages/web3wallet/test/sign.spec.ts @@ -4,9 +4,10 @@ import { formatJsonRpcResult, isJsonRpcRequest, } from "@walletconnect/jsonrpc-utils"; -import { SignClient } from "@walletconnect/sign-client"; +import { SignClient, ENGINE_RPC_OPTS } from "@walletconnect/sign-client"; import { CoreTypes, ICore, ISignClient, SessionTypes } from "@walletconnect/types"; import { getSdkError } from "@walletconnect/utils"; +import { toMiliseconds } from "@walletconnect/time"; import { Wallet as CryptoWallet } from "@ethersproject/wallet"; import { expect, describe, it, beforeEach, vi, beforeAll, afterAll } from "vitest"; @@ -383,6 +384,108 @@ describe("Sign Integration", () => { ]); }); + it("receive proposal_expire event", async () => { + const { uri: uriString } = await dapp.connect({ requiredNamespaces: TEST_REQUIRED_NAMESPACES }); + let startTimer; + // first pair and approve session + await Promise.all([ + new Promise((resolve) => { + wallet.once("session_proposal", () => { + vi.useFakeTimers({ + shouldAdvanceTime: true, + shouldClearNativeTimers: true, + }); + // Fast-forward system time by 4 min 58 seconds after expiry was first set. + vi.setSystemTime( + Date.now() + toMiliseconds(ENGINE_RPC_OPTS.wc_sessionPropose.req.ttl - 2), + ); + startTimer = Date.now(); + }); + wallet.on("session_proposal", async (event) => { + const { id } = event; + await new Promise((resolve) => { + wallet.on("proposal_expire", (event) => { + const { id: expiredId } = event; + if (id === expiredId) { + expect(startTimer).to.be.approximately(Date.now(), 5000); // 5 seconds delta for heartbeat + resolve(); + } + }); + }); + resolve(); + }); + }), + wallet.pair({ uri: uriString! }), + ]); + vi.useRealTimers(); + }); + it("receive session_request_expire event", async () => { + // first pair and approve session + await Promise.all([ + new Promise((resolve) => { + wallet.on("session_proposal", async (sessionProposal) => { + const { id, params } = sessionProposal; + session = await wallet.approveSession({ + id, + namespaces: { + eip155: { + ...TEST_NAMESPACES.eip155, + accounts: [`${TEST_ETHEREUM_CHAIN}:${cryptoWallet.address}`], + }, + }, + }); + expect(params.requiredNamespaces).to.toMatchObject(TEST_REQUIRED_NAMESPACES); + resolve(session); + }); + }), + sessionApproval(), + wallet.pair({ uri: uriString }), + ]); + let startTimer; + // first pair and approve session + await Promise.all([ + new Promise((resolve) => { + wallet.once("session_request", () => { + vi.useFakeTimers({ + shouldAdvanceTime: true, + shouldClearNativeTimers: true, + }); + // Fast-forward system time by 4 min 58 seconds after expiry was first set. + vi.setSystemTime( + Date.now() + toMiliseconds(ENGINE_RPC_OPTS.wc_sessionRequest.req.ttl - 2), + ); + startTimer = Date.now(); + }); + wallet.on("session_request", async (event) => { + const { id } = event; + await new Promise((resolve) => { + wallet.on("session_request_expire", (event) => { + const { id: expiredId } = event; + if (id === expiredId) { + expect(startTimer).to.be.approximately(Date.now(), 5000); // 5 seconds delta for heartbeat + resolve(); + } + }); + }); + await wallet.respondSessionRequest({ + topic: session.topic, + response: formatJsonRpcResult(id, "0x"), + }); + resolve(); + }); + }), + dapp.request({ + topic: session.topic, + request: { + method: "eth_signTransaction", + params: ["0xdeadbeef", cryptoWallet.address], + }, + chainId: TEST_ETHEREUM_CHAIN, + }), + ]); + vi.useRealTimers(); + }); + it("should get pending session requests", async () => { // first pair and approve session await Promise.all([ From f7a777c5252cc0b23c6b6daef3d7e2730a0ecbb3 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 29 Jan 2024 12:12:41 +0200 Subject: [PATCH 33/36] fix: starts timer once request is received --- packages/web3wallet/test/sign.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/web3wallet/test/sign.spec.ts b/packages/web3wallet/test/sign.spec.ts index 5723b5917..f7fa67fc2 100644 --- a/packages/web3wallet/test/sign.spec.ts +++ b/packages/web3wallet/test/sign.spec.ts @@ -386,7 +386,7 @@ describe("Sign Integration", () => { it("receive proposal_expire event", async () => { const { uri: uriString } = await dapp.connect({ requiredNamespaces: TEST_REQUIRED_NAMESPACES }); - let startTimer; + // first pair and approve session await Promise.all([ new Promise((resolve) => { @@ -399,10 +399,10 @@ describe("Sign Integration", () => { vi.setSystemTime( Date.now() + toMiliseconds(ENGINE_RPC_OPTS.wc_sessionPropose.req.ttl - 2), ); - startTimer = Date.now(); }); wallet.on("session_proposal", async (event) => { const { id } = event; + const startTimer = Date.now(); await new Promise((resolve) => { wallet.on("proposal_expire", (event) => { const { id: expiredId } = event; @@ -441,7 +441,6 @@ describe("Sign Integration", () => { sessionApproval(), wallet.pair({ uri: uriString }), ]); - let startTimer; // first pair and approve session await Promise.all([ new Promise((resolve) => { @@ -454,10 +453,10 @@ describe("Sign Integration", () => { vi.setSystemTime( Date.now() + toMiliseconds(ENGINE_RPC_OPTS.wc_sessionRequest.req.ttl - 2), ); - startTimer = Date.now(); }); wallet.on("session_request", async (event) => { const { id } = event; + const startTimer = Date.now(); await new Promise((resolve) => { wallet.on("session_request_expire", (event) => { const { id: expiredId } = event; From 33de2133a879f3da2686b34a58407d331b511004 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 5 Feb 2024 19:37:30 +0200 Subject: [PATCH 34/36] fix: handles chainChanged with string chainId value --- providers/universal-provider/src/utils/misc.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/providers/universal-provider/src/utils/misc.ts b/providers/universal-provider/src/utils/misc.ts index fe2b874fc..941c1a894 100644 --- a/providers/universal-provider/src/utils/misc.ts +++ b/providers/universal-provider/src/utils/misc.ts @@ -125,10 +125,12 @@ export function populateNamespacesChains( return parsedNamespaces; } -export function convertChainIdToNumber(chainId: string | number): number { +export function convertChainIdToNumber(chainId: string | number): number | string { if (typeof chainId === "number") return chainId; if (chainId.includes("0x")) { return parseInt(chainId, 16); } - return chainId.includes(":") ? Number(chainId.split(":")[1]) : Number(chainId); + + chainId = chainId.includes(":") ? chainId.split(":")[1] : chainId; + return isNaN(Number(chainId)) ? chainId : Number(chainId); } From e9d14be5a6cf3e7d92fd63073333a62b232d3ec6 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 5 Feb 2024 19:37:50 +0200 Subject: [PATCH 35/36] fix: assigns approved namespaces by wallet --- .../src/UniversalProvider.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/providers/universal-provider/src/UniversalProvider.ts b/providers/universal-provider/src/UniversalProvider.ts index aabda0778..e400f4811 100644 --- a/providers/universal-provider/src/UniversalProvider.ts +++ b/providers/universal-provider/src/UniversalProvider.ts @@ -186,10 +186,9 @@ export class UniversalProvider implements IUniversalProvider { .then((session) => { this.session = session; // assign namespaces from session if not already defined - if (!this.namespaces) { - this.namespaces = populateNamespacesChains(session.namespaces) as NamespaceConfig; - this.persist("namespaces", this.namespaces); - } + const approved = populateNamespacesChains(session.namespaces) as NamespaceConfig; + this.namespaces = mergeRequiredOptionalNamespaces(this.namespaces, approved); + this.persist("namespaces", this.namespaces); }) .catch((error) => { if (error.message !== PROPOSAL_EXPIRY_MESSAGE) { @@ -373,6 +372,7 @@ export class UniversalProvider implements IUniversalProvider { convertChainIdToNumber(requestChainId) !== convertChainIdToNumber(payloadChainId) ? `${namespace}:${convertChainIdToNumber(payloadChainId)}` : requestChainId; + this.onChainChanged(chainIdToProcess); } else { this.events.emit(event.name, event.data); @@ -467,12 +467,21 @@ export class UniversalProvider implements IUniversalProvider { const [namespace, chainId] = this.validateChain(caip2Chain); + if (!chainId) return; + if (!internal) { this.getProvider(namespace).setDefaultChain(chainId); } - (this.namespaces[namespace] ?? this.namespaces[`${namespace}:${chainId}`]).defaultChain = - chainId; + if (this.namespaces[namespace]) { + this.namespaces[namespace].defaultChain = chainId; + } else if (this.namespaces[`${namespace}:${chainId}`]) { + this.namespaces[`${namespace}:${chainId}`].defaultChain = chainId; + } else { + // @ts-ignore + this.namespaces[`${namespace}:${chainId}`] = { defaultChain: chainId }; + } + this.persist("namespaces", this.namespaces); this.events.emit("chainChanged", chainId); } From 2590676f9c86a8ffa4b938cd511a2cfafce34df1 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Mon, 5 Feb 2024 19:38:47 +0200 Subject: [PATCH 36/36] fix: tests --- .../universal-provider/test/index.spec.ts | 164 +++++++++++++++++- 1 file changed, 160 insertions(+), 4 deletions(-) diff --git a/providers/universal-provider/test/index.spec.ts b/providers/universal-provider/test/index.spec.ts index 57927bcc3..421ad8389 100644 --- a/providers/universal-provider/test/index.spec.ts +++ b/providers/universal-provider/test/index.spec.ts @@ -848,6 +848,159 @@ describe("UniversalProvider", function () { expectedChainId: chains[1], }); }); + it("should handle switch chain event on non required namespaces", async () => { + const dapp = await UniversalProvider.init({ + ...TEST_PROVIDER_OPTS, + name: "dapp", + }); + const wallet = await UniversalProvider.init({ + ...TEST_PROVIDER_OPTS, + name: "wallet", + }); + const chains = ["eip155:1", "eip155:2"]; + const solanaChains = [ + "solana:91b171bb158e2d3848fa23a9f1c25182", + "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ", + ]; + await testConnectMethod( + { + dapp, + wallet, + }, + { + requiredNamespaces: { + "eip155:1": { + methods, + events, + }, + }, + optionalNamespaces: {}, + namespaces: { + eip155: { + accounts: chains.map((chain) => `${chain}:${walletAddress}`), + methods, + events, + }, + solana: { + accounts: solanaChains.map((chain) => `${chain}:${walletAddress}`), + methods, + events, + }, + }, + }, + ); + const expectedChainId = solanaChains[1]; + await Promise.all([ + new Promise((resolve) => { + dapp.on("chainChanged", (chainId: any) => { + expect(chainId).to.eql(expectedChainId.split(":")[1]); + resolve(); + }); + }), + wallet.client.emit({ + topic: dapp.session?.topic || "", + event: { + name: "chainChanged", + data: expectedChainId, + }, + chainId: expectedChainId, + }), + ]); + + await validateProvider({ + provider: dapp, + chains: solanaChains, + addresses: [walletAddress], + defaultNamespace: "solana", + expectedChainId, + }); + }); + it("should handle requesting x & y as required & optional chains while wallet approves x + y + z", async () => { + const dapp = await UniversalProvider.init({ + ...TEST_PROVIDER_OPTS, + name: "dapp", + }); + const wallet = await UniversalProvider.init({ + ...TEST_PROVIDER_OPTS, + name: "wallet", + }); + const chains = { + eip155: ["eip155:1", "eip155:2"], + solana: [ + "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ", + "solana:91b171bb158e2d3848fa23a9f1c25182", + ], + cosmos: ["cosmos:hub1", "cosmos:hub2"], + }; + + await testConnectMethod( + { + dapp, + wallet, + }, + { + requiredNamespaces: { + eip155: { + chains: chains.eip155, + methods, + events, + }, + }, + optionalNamespaces: { + solana: { + chains: chains.solana, + methods, + events, + }, + }, + namespaces: { + eip155: { + accounts: chains.eip155.map((chain) => `${chain}:${walletAddress}`), + methods, + events, + }, + solana: { + accounts: chains.solana.map((chain) => `${chain}:${walletAddress}`), + methods, + events, + }, + cosmos: { + accounts: chains.cosmos.map((chain) => `${chain}:${walletAddress}`), + methods, + events, + }, + }, + }, + ); + const expectedChainId = chains.solana[1]; + await Promise.all([ + new Promise((resolve) => { + dapp.on("chainChanged", (chainId: any) => { + expect(chainId).to.eql(expectedChainId.split(":")[1]); + resolve(); + }); + }), + wallet.client.emit({ + topic: dapp.session?.topic || "", + event: { + name: "chainChanged", + data: expectedChainId, + }, + chainId: expectedChainId, + }), + ]); + + // validate that provider is created for each approed namespace + expect(Object.keys(dapp.rpcProviders).length).to.eql(Object.keys(chains).length); + + await validateProvider({ + provider: dapp, + chains: chains.solana, + addresses: [walletAddress], + defaultNamespace: "solana", + expectedChainId, + }); + }); }); }); @@ -948,15 +1101,18 @@ const validateProvider = async (params: ValidateProviderParams) => { if (addresses) { expect(accounts).to.toMatchObject(addresses); } - const chain = await provider.request({ method: "eth_chainId" }); - expect(chain).to.not.be.null; + if (chains) { - expect(chains).toContain(`${defaultNamespace}:${chain}`); expect(Object.keys(provider.rpcProviders[defaultNamespace].httpProviders)).to.toMatchObject( chains.map((c) => c.split(":")[1]), ); } if (expectedChainId) { - expect(expectedChainId).to.equal(`${defaultNamespace}:${chain}`); + const chainId = provider.rpcProviders[defaultNamespace].getDefaultChain(); + expect(chainId).to.not.be.null; + expect(expectedChainId).to.equal(`${defaultNamespace}:${chainId}`); + if (chains) { + expect(chains).to.include(`${defaultNamespace}:${chainId}`); + } } };