From 642f63c37679e888a74e654e741e10f6a126a2be Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 20:39:17 -0800 Subject: [PATCH 01/50] asdf --- ...letLinkConnection.ts => WalletLinkHTTP.ts} | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) rename packages/wallet-sdk/src/connection/{WalletLinkConnection.ts => WalletLinkHTTP.ts} (91%) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts similarity index 91% rename from packages/wallet-sdk/src/connection/WalletLinkConnection.ts rename to packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index 4c92723cb0..db27990daa 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -328,6 +328,7 @@ export class WalletLinkConnection { } private shouldFetchUnseenEventsOnConnect = false; + private http = new WalletLinkHTTP(); public async checkUnseenEvents() { if (!this.connected) { @@ -345,47 +346,7 @@ export class WalletLinkConnection { private async fetchUnseenEventsAPI() { this.shouldFetchUnseenEventsOnConnect = false; - - const credentials = `${this.sessionId}:${this.sessionKey}`; - const auth = `Basic ${btoa(credentials)}`; - - const response = await fetch(`${this.linkAPIUrl}/events?unseen=true`, { - headers: { - Authorization: auth, - }, - }); - - if (response.ok) { - const { events, error } = (await response.json()) as { - events?: { - id: string; - event: 'Web3Request' | 'Web3Response' | 'Web3RequestCanceled'; - data: string; - }[]; - timestamp: number; - error?: string; - }; - - if (error) { - throw new Error(`Check unseen events failed: ${error}`); - } - - const responseEvents: ServerMessageEvent[] = - events - ?.filter((e) => e.event === 'Web3Response') - .map((e) => ({ - type: 'Event', - sessionId: this.sessionId, - eventId: e.id, - event: e.event, - data: e.data, - })) ?? []; - responseEvents.forEach((e) => this.handleIncomingEvent(e)); - - this.markUnseenEventsAsSeen(responseEvents); - } else { - throw new Error(`Check unseen events failed: ${response.status}`); - } + await this.http.fetchUnseenEventsAPI(); } /** From 17566f422c45e81977757fa3a0d8d3684669ea79 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 20:41:06 -0800 Subject: [PATCH 02/50] asdf --- .../src/connection/WalletLinkHTTP.ts | 504 ++---------------- 1 file changed, 38 insertions(+), 466 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index db27990daa..807252a59b 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -1,472 +1,44 @@ -// Copyright (c) 2018-2023 Coinbase, Inc. -// Licensed under the Apache License, version 2.0 - -import { Session } from '../relay/Session'; -import { IntNumber } from '../types'; -import { - ClientMessage, - ClientMessageGetSessionConfig, - ClientMessageHostSession, - ClientMessageIsLinked, - ClientMessagePublishEvent, - ClientMessageSetSessionConfig, -} from './ClientMessage'; -import { DiagnosticLogger, EVENTS } from './DiagnosticLogger'; -import { - isServerMessageFail, - ServerMessage, - ServerMessageEvent, - ServerMessageFail, - ServerMessageGetSessionConfigOK, - ServerMessageIsLinkedOK, - ServerMessageLinked, - ServerMessageOK, - ServerMessagePublishEventOK, - ServerMessageSessionConfigUpdated, -} from './ServerMessage'; -import { SessionConfig } from './SessionConfig'; -import { ConnectionState, WalletLinkWebSocket } from './WalletLinkWebSocket'; - -const HEARTBEAT_INTERVAL = 10000; -const REQUEST_TIMEOUT = 60000; - -/** - * Coinbase Wallet Connection - */ -export class WalletLinkConnection { - private ws: WalletLinkWebSocket; - private destroyed = false; - private lastHeartbeatResponse = 0; - private nextReqId = IntNumber(1); - - private sessionConfigListener?: (_: SessionConfig) => void; - setSessionConfigListener(listener: (_: SessionConfig) => void): void { - this.sessionConfigListener = listener; - } - - private linkedListener?: (_: boolean) => void; - setLinkedListener(listener: (_: boolean) => void): void { - this.linkedListener = listener; - } - - private connectedListener?: (_: boolean) => void; - setConnectedListener(listener: (_: boolean) => void): void { - this.connectedListener = listener; - } - - private incomingEventListener?: (_: ServerMessageEvent) => void; - setIncomingEventListener(listener: (_: ServerMessageEvent) => void): void { - this.incomingEventListener = listener; - } - - /** - * Constructor - * @param sessionId Session ID - * @param sessionKey Session Key - * @param linkAPIUrl Coinbase Wallet link server URL - * @param [WebSocketClass] Custom WebSocket implementation - */ - constructor( - private sessionId: string, - private sessionKey: string, - private linkAPIUrl: string, - private diagnostic?: DiagnosticLogger, - WebSocketClass: typeof WebSocket = WebSocket - ) { - const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); - this.ws = ws; - - this.ws.setConnectionStateListener(async (state) => { - // attempt to reconnect every 5 seconds when disconnected - this.diagnostic?.log(EVENTS.CONNECTED_STATE_CHANGE, { - state, - sessionIdHash: Session.hash(sessionId), - }); - - let connected = false; - switch (state) { - case ConnectionState.DISCONNECTED: - // if DISCONNECTED and not destroyed - if (!this.destroyed) { - const connect = async () => { - // wait 5 seconds - await new Promise((resolve) => setTimeout(resolve, 5000)); - // check whether it's destroyed again - if (!this.destroyed) { - // reconnect - ws.connect().catch(() => { - connect(); - }); - } - }; - connect(); - } - break; - - case ConnectionState.CONNECTED: - // perform authentication upon connection - try { - // if CONNECTED, authenticate, and then check link status - await this.authenticate(); - this.sendIsLinked(); - this.sendGetSessionConfig(); - connected = true; - } catch { - /* empty */ - } - - // send heartbeat every n seconds while connected - // if CONNECTED, start the heartbeat timer - // first timer event updates lastHeartbeat timestamp - // subsequent calls send heartbeat message - this.updateLastHeartbeat(); - setInterval(() => { - this.heartbeat(); - }, HEARTBEAT_INTERVAL); - - // check for unseen events - if (this.shouldFetchUnseenEventsOnConnect) { - this.fetchUnseenEventsAPI(); - } - break; - - case ConnectionState.CONNECTING: - break; - } - - // distinctUntilChanged - if (this.connected !== connected) { - this.connected = connected; - } - }); - - ws.setIncomingDataListener((m) => { - switch (m.type) { - // handle server's heartbeat responses - case 'Heartbeat': - this.updateLastHeartbeat(); - return; - - // handle link status updates - case 'IsLinkedOK': - case 'Linked': { - const msg = m as Omit & ServerMessageLinked; - this.diagnostic?.log(EVENTS.LINKED, { - sessionIdHash: Session.hash(sessionId), - linked: msg.linked, - type: m.type, - onlineGuests: msg.onlineGuests, - }); - - this.linked = msg.linked || msg.onlineGuests > 0; - break; - } - - // handle session config updates - case 'GetSessionConfigOK': - case 'SessionConfigUpdated': { - const msg = m as Omit & - ServerMessageSessionConfigUpdated; - this.diagnostic?.log(EVENTS.SESSION_CONFIG_RECEIVED, { - sessionIdHash: Session.hash(sessionId), - metadata_keys: msg && msg.metadata ? Object.keys(msg.metadata) : undefined, - }); - this.sessionConfigListener?.({ - webhookId: msg.webhookId, - webhookUrl: msg.webhookUrl, - metadata: msg.metadata, - }); - break; - } - - case 'Event': { - this.handleIncomingEvent(m); - break; - } - } - - // resolve request promises - if (m.id !== undefined) { - this.requestResolutions.get(m.id)?.(m); - } - }); - } - - // mark unseen events as seen - private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { +class WalletLinkHTTP { + async fetchUnseenEventsAPI() { const credentials = `${this.sessionId}:${this.sessionKey}`; const auth = `Basic ${btoa(credentials)}`; - Promise.all( - events.map((e) => - fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { - method: 'POST', - headers: { - Authorization: auth, - }, - }) - ) - ).catch((error) => console.error('Unabled to mark event as failed:', error)); - } - - /** - * Make a connection to the server - */ - public connect(): void { - if (this.destroyed) { - throw new Error('instance is destroyed'); - } - this.diagnostic?.log(EVENTS.STARTED_CONNECTING, { - sessionIdHash: Session.hash(this.sessionId), - }); - this.ws.connect(); - } - - /** - * Terminate connection, and mark as destroyed. To reconnect, create a new - * instance of WalletSDKConnection - */ - public destroy(): void { - this.destroyed = true; - - this.ws.disconnect(); - this.diagnostic?.log(EVENTS.DISCONNECTED, { - sessionIdHash: Session.hash(this.sessionId), - }); - - this.sessionConfigListener = undefined; - this.connectedListener = undefined; - this.linkedListener = undefined; - this.incomingEventListener = undefined; - } - - public get isDestroyed(): boolean { - return this.destroyed; - } - - /** - * true if connected and authenticated, else false - * runs listener when connected status changes - */ - private _connected = false; - private get connected(): boolean { - return this._connected; - } - private set connected(connected: boolean) { - this._connected = connected; - if (connected) this.onceConnected?.(); - this.connectedListener?.(connected); - } - - /** - * Execute once when connected - */ - private onceConnected?: () => void; - private setOnceConnected(callback: () => Promise): Promise { - return new Promise((resolve) => { - if (this.connected) { - callback().then(resolve); - } else { - this.onceConnected = () => { - callback().then(resolve); - this.onceConnected = undefined; - }; - } - }); - } - - /** - * true if linked (a guest has joined before) - * runs listener when linked status changes - */ - private _linked = false; - private get linked(): boolean { - return this._linked; - } - private set linked(linked: boolean) { - this._linked = linked; - if (linked) this.onceLinked?.(); - this.linkedListener?.(linked); - } - - /** - * Execute once when linked - */ - private onceLinked?: () => void; - private setOnceLinked(callback: () => Promise): Promise { - return new Promise((resolve) => { - if (this.linked) { - callback().then(resolve); - } else { - this.onceLinked = () => { - callback().then(resolve); - this.onceLinked = undefined; - }; - } - }); - } - - private handleIncomingEvent(m: ServerMessage) { - function isServerMessageEvent(msg: ServerMessage): msg is ServerMessageEvent { - if (msg.type !== 'Event') { - return false; - } - const sme = msg as ServerMessageEvent; - return ( - typeof sme.sessionId === 'string' && - typeof sme.eventId === 'string' && - typeof sme.event === 'string' && - typeof sme.data === 'string' - ); - } - - if (!isServerMessageEvent(m)) { - return; + const response = await fetch(`${this.linkAPIUrl}/events?unseen=true`, { + headers: { + Authorization: auth, + }, + }); + + if (response.ok) { + const { events, error } = (await response.json()) as { + events?: { + id: string; + event: 'Web3Request' | 'Web3Response' | 'Web3RequestCanceled'; + data: string; + }[]; + timestamp: number; + error?: string; + }; + + if (error) { + throw new Error(`Check unseen events failed: ${error}`); + } + + const responseEvents: ServerMessageEvent[] = + events + ?.filter((e) => e.event === 'Web3Response') + .map((e) => ({ + type: 'Event', + sessionId: this.sessionId, + eventId: e.id, + event: e.event, + data: e.data, + })) ?? []; + responseEvents.forEach((e) => this.handleIncomingEvent(e)); + + this.markUnseenEventsAsSeen(responseEvents); + } else { + throw new Error(`Check unseen events failed: ${response.status}`); } - - this.incomingEventListener?.(m); - } - - private shouldFetchUnseenEventsOnConnect = false; - private http = new WalletLinkHTTP(); - - public async checkUnseenEvents() { - if (!this.connected) { - this.shouldFetchUnseenEventsOnConnect = true; - return; - } - - await new Promise((resolve) => setTimeout(resolve, 250)); - try { - await this.fetchUnseenEventsAPI(); - } catch (e) { - console.error('Unable to check for unseen events', e); - } - } - - private async fetchUnseenEventsAPI() { - this.shouldFetchUnseenEventsOnConnect = false; - await this.http.fetchUnseenEventsAPI(); - } - - /** - * Set session metadata in SessionConfig object - * @param key - * @param value - * @returns a Promise that completes when successful - */ - public async setSessionMetadata(key: string, value: string | null) { - const message = ClientMessageSetSessionConfig({ - id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, - metadata: { [key]: value }, - }); - - return this.setOnceConnected(async () => { - const res = await this.makeRequest(message); - if (isServerMessageFail(res)) { - throw new Error(res.error || 'failed to set session metadata'); - } - }); - } - - /** - * Publish an event and emit event ID when successful - * @param event event name - * @param data event data - * @param callWebhook whether the webhook should be invoked - * @returns a Promise that emits event ID when successful - */ - public async publishEvent(event: string, data: string, callWebhook = false) { - const message = ClientMessagePublishEvent({ - id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, - event, - data, - callWebhook, - }); - - return this.setOnceLinked(async () => { - const res = await this.makeRequest(message); - if (isServerMessageFail(res)) { - throw new Error(res.error || 'failed to publish event'); - } - return res.eventId; - }); - } - - private sendData(message: ClientMessage): void { - this.ws.sendData(JSON.stringify(message)); - } - - private updateLastHeartbeat(): void { - this.lastHeartbeatResponse = Date.now(); - } - - private heartbeat(): void { - if (Date.now() - this.lastHeartbeatResponse > HEARTBEAT_INTERVAL * 2) { - this.ws.disconnect(); - return; - } - try { - this.ws.sendData('h'); - } catch { - // noop - } - } - - private requestResolutions = new Map void>(); - - private async makeRequest( - message: ClientMessage, - timeout: number = REQUEST_TIMEOUT - ): Promise { - const reqId = message.id; - this.sendData(message); - - // await server message with corresponding id - let timeoutId: number; - return Promise.race([ - new Promise((_, reject) => { - timeoutId = window.setTimeout(() => { - reject(new Error(`request ${reqId} timed out`)); - }, timeout); - }), - new Promise((resolve) => { - this.requestResolutions.set(reqId, (m) => { - clearTimeout(timeoutId); // clear the timeout - resolve(m as T); - this.requestResolutions.delete(reqId); - }); - }), - ]); - } - - private async authenticate() { - const msg = ClientMessageHostSession({ - id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, - sessionKey: this.sessionKey, - }); - const res = await this.makeRequest(msg); - if (isServerMessageFail(res)) { - throw new Error(res.error || 'failed to authentcate'); - } - } - - private sendIsLinked(): void { - const msg = ClientMessageIsLinked({ - id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, - }); - this.sendData(msg); - } - - private sendGetSessionConfig(): void { - const msg = ClientMessageGetSessionConfig({ - id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, - }); - this.sendData(msg); } } From 948ec7e8d1b2502f27ca51d05050a57a6bd3c724 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 20:43:43 -0800 Subject: [PATCH 03/50] asdf --- .../src/connection/WalletLinkConnection.ts | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 4c92723cb0..3cd03a77ae 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -345,47 +345,6 @@ export class WalletLinkConnection { private async fetchUnseenEventsAPI() { this.shouldFetchUnseenEventsOnConnect = false; - - const credentials = `${this.sessionId}:${this.sessionKey}`; - const auth = `Basic ${btoa(credentials)}`; - - const response = await fetch(`${this.linkAPIUrl}/events?unseen=true`, { - headers: { - Authorization: auth, - }, - }); - - if (response.ok) { - const { events, error } = (await response.json()) as { - events?: { - id: string; - event: 'Web3Request' | 'Web3Response' | 'Web3RequestCanceled'; - data: string; - }[]; - timestamp: number; - error?: string; - }; - - if (error) { - throw new Error(`Check unseen events failed: ${error}`); - } - - const responseEvents: ServerMessageEvent[] = - events - ?.filter((e) => e.event === 'Web3Response') - .map((e) => ({ - type: 'Event', - sessionId: this.sessionId, - eventId: e.id, - event: e.event, - data: e.data, - })) ?? []; - responseEvents.forEach((e) => this.handleIncomingEvent(e)); - - this.markUnseenEventsAsSeen(responseEvents); - } else { - throw new Error(`Check unseen events failed: ${response.status}`); - } } /** From 5d016ecfadd05d7fea75081b8d4f45a2a106ecb2 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 21:00:56 -0800 Subject: [PATCH 04/50] separate out http stuff --- .../src/connection/WalletLinkConnection.ts | 20 +++---------- .../src/connection/WalletLinkHTTP.ts | 28 ++++++++++++++++++- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 3cd03a77ae..b22ebfcc98 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -25,6 +25,7 @@ import { ServerMessageSessionConfigUpdated, } from './ServerMessage'; import { SessionConfig } from './SessionConfig'; +import { WalletLinkHTTP } from './WalletLinkHTTP'; import { ConnectionState, WalletLinkWebSocket } from './WalletLinkWebSocket'; const HEARTBEAT_INTERVAL = 10000; @@ -35,6 +36,7 @@ const REQUEST_TIMEOUT = 60000; */ export class WalletLinkConnection { private ws: WalletLinkWebSocket; + private http: WalletLinkHTTP; private destroyed = false; private lastHeartbeatResponse = 0; private nextReqId = IntNumber(1); @@ -190,23 +192,8 @@ export class WalletLinkConnection { this.requestResolutions.get(m.id)?.(m); } }); - } - // mark unseen events as seen - private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { - const credentials = `${this.sessionId}:${this.sessionKey}`; - const auth = `Basic ${btoa(credentials)}`; - - Promise.all( - events.map((e) => - fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { - method: 'POST', - headers: { - Authorization: auth, - }, - }) - ) - ).catch((error) => console.error('Unabled to mark event as failed:', error)); + this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey, this.handleIncomingEvent); } /** @@ -345,6 +332,7 @@ export class WalletLinkConnection { private async fetchUnseenEventsAPI() { this.shouldFetchUnseenEventsOnConnect = false; + this.http.fetchUnseenEventsAPI(); } /** diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index 807252a59b..cd1b0e7b0c 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -1,4 +1,13 @@ -class WalletLinkHTTP { +import { ServerMessage, ServerMessageEvent } from './ServerMessage'; + +export class WalletLinkHTTP { + constructor( + private readonly linkAPIUrl: string, + private readonly sessionId: string, + private readonly sessionKey: string, + private readonly handleIncomingEvent: (_: ServerMessage) => void + ) {} + async fetchUnseenEventsAPI() { const credentials = `${this.sessionId}:${this.sessionKey}`; const auth = `Basic ${btoa(credentials)}`; @@ -41,4 +50,21 @@ class WalletLinkHTTP { throw new Error(`Check unseen events failed: ${response.status}`); } } + + // mark unseen events as seen + private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { + const credentials = `${this.sessionId}:${this.sessionKey}`; + const auth = `Basic ${btoa(credentials)}`; + + Promise.all( + events.map((e) => + fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { + method: 'POST', + headers: { + Authorization: auth, + }, + }) + ) + ).catch((error) => console.error('Unabled to mark event as failed:', error)); + } } From af4182996a5ac81b5113a4632645d3e427a70476 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 21:05:07 -0800 Subject: [PATCH 05/50] swap --- .../src/connection/WalletLinkHTTP.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index cd1b0e7b0c..dd1ab1644b 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -8,6 +8,23 @@ export class WalletLinkHTTP { private readonly handleIncomingEvent: (_: ServerMessage) => void ) {} + // mark unseen events as seen + private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { + const credentials = `${this.sessionId}:${this.sessionKey}`; + const auth = `Basic ${btoa(credentials)}`; + + Promise.all( + events.map((e) => + fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { + method: 'POST', + headers: { + Authorization: auth, + }, + }) + ) + ).catch((error) => console.error('Unabled to mark event as failed:', error)); + } + async fetchUnseenEventsAPI() { const credentials = `${this.sessionId}:${this.sessionKey}`; const auth = `Basic ${btoa(credentials)}`; @@ -50,21 +67,4 @@ export class WalletLinkHTTP { throw new Error(`Check unseen events failed: ${response.status}`); } } - - // mark unseen events as seen - private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { - const credentials = `${this.sessionId}:${this.sessionKey}`; - const auth = `Basic ${btoa(credentials)}`; - - Promise.all( - events.map((e) => - fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { - method: 'POST', - headers: { - Authorization: auth, - }, - }) - ) - ).catch((error) => console.error('Unabled to mark event as failed:', error)); - } } From 6ce9358865baae14649f66cfc535fe1d5ac7e6c1 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 21:08:46 -0800 Subject: [PATCH 06/50] linkAPIUrl no need after init --- packages/wallet-sdk/src/connection/WalletLinkConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index b22ebfcc98..70459d5273 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -71,7 +71,7 @@ export class WalletLinkConnection { constructor( private sessionId: string, private sessionKey: string, - private linkAPIUrl: string, + linkAPIUrl: string, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { From c1cf182be2b79bb8916c3d6170434752e6f2ab3d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 21:21:50 -0800 Subject: [PATCH 07/50] cleanup --- .../src/connection/WalletLinkHTTP.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index dd1ab1644b..a2b5380a7a 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -1,37 +1,35 @@ -import { ServerMessage, ServerMessageEvent } from './ServerMessage'; +import { ServerMessageEvent } from './ServerMessage'; export class WalletLinkHTTP { + private readonly auth: string; + constructor( private readonly linkAPIUrl: string, private readonly sessionId: string, - private readonly sessionKey: string, - private readonly handleIncomingEvent: (_: ServerMessage) => void - ) {} + sessionKey: string + ) { + const credentials = `${sessionId}:${sessionKey}`; + this.auth = `Basic ${btoa(credentials)}`; + } // mark unseen events as seen private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { - const credentials = `${this.sessionId}:${this.sessionKey}`; - const auth = `Basic ${btoa(credentials)}`; - Promise.all( events.map((e) => fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { method: 'POST', headers: { - Authorization: auth, + Authorization: this.auth, }, }) ) ).catch((error) => console.error('Unabled to mark event as failed:', error)); } - async fetchUnseenEventsAPI() { - const credentials = `${this.sessionId}:${this.sessionKey}`; - const auth = `Basic ${btoa(credentials)}`; - + async fetchUnseenEventsAPI(): Promise { const response = await fetch(`${this.linkAPIUrl}/events?unseen=true`, { headers: { - Authorization: auth, + Authorization: this.auth, }, }); @@ -60,11 +58,11 @@ export class WalletLinkHTTP { event: e.event, data: e.data, })) ?? []; - responseEvents.forEach((e) => this.handleIncomingEvent(e)); this.markUnseenEventsAsSeen(responseEvents); - } else { - throw new Error(`Check unseen events failed: ${response.status}`); + + return responseEvents; } + throw new Error(`Check unseen events failed: ${response.status}`); } } From 479f55160aa320929ad5a1bf74815514b6d50a7a Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 21:27:44 -0800 Subject: [PATCH 08/50] for each --- packages/wallet-sdk/src/connection/WalletLinkConnection.ts | 4 +++- packages/wallet-sdk/src/connection/WalletLinkHTTP.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 70459d5273..729c248080 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -332,7 +332,9 @@ export class WalletLinkConnection { private async fetchUnseenEventsAPI() { this.shouldFetchUnseenEventsOnConnect = false; - this.http.fetchUnseenEventsAPI(); + + const responseEvents = await this.http.fetchUnseenEvents(); + responseEvents.forEach((e) => this.handleIncomingEvent(e)); } /** diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index a2b5380a7a..f1aecf3c52 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -26,7 +26,7 @@ export class WalletLinkHTTP { ).catch((error) => console.error('Unabled to mark event as failed:', error)); } - async fetchUnseenEventsAPI(): Promise { + async fetchUnseenEvents(): Promise { const response = await fetch(`${this.linkAPIUrl}/events?unseen=true`, { headers: { Authorization: this.auth, From 2d700f0b5cab51f3ecdabbfdccf9e808602b7a8e Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 21:28:59 -0800 Subject: [PATCH 09/50] constructor --- packages/wallet-sdk/src/connection/WalletLinkConnection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 729c248080..091c6c05bb 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -78,6 +78,8 @@ export class WalletLinkConnection { const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); this.ws = ws; + this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); + this.ws.setConnectionStateListener(async (state) => { // attempt to reconnect every 5 seconds when disconnected this.diagnostic?.log(EVENTS.CONNECTED_STATE_CHANGE, { @@ -192,8 +194,6 @@ export class WalletLinkConnection { this.requestResolutions.get(m.id)?.(m); } }); - - this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey, this.handleIncomingEvent); } /** From 86685314a722ab3f72cdaa37123a6e5373eb0b0c Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 22:14:04 -0800 Subject: [PATCH 10/50] remove no-op --- .../src/relay/WalletLinkRelay.test.ts | 19 ------------------- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 10 ---------- 2 files changed, 29 deletions(-) diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts index d271bd8c3c..0697ad5bda 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts @@ -93,25 +93,6 @@ describe('WalletLinkRelay', () => { expect(setSessionConfigListenerSpy).toHaveBeenCalled(); }); - it('should handle session config changes', async () => { - const sessionConfig: SessionConfig = { - webhookId: 'webhookId', - webhookUrl: 'webhookUrl', - metadata: {}, - }; - - const relay = new WalletLinkRelay(options); - - const onSessionConfigChangedSpy = jest.spyOn(relay, 'onSessionConfigChanged'); - - (relay as any).connection.ws.incomingDataListener?.({ - ...sessionConfig, - type: 'GetSessionConfigOK', - }); - - expect(onSessionConfigChangedSpy).toHaveBeenCalledWith(sessionConfig); - }); - it('should update metadata with setSessionConfigListener', async () => { const sessionConfig: SessionConfig = { webhookId: 'webhookId', diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 1231bdda73..0d28e73b1d 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -174,14 +174,6 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { }); connection.setSessionConfigListener((c: SessionConfig) => { - try { - this.onSessionConfigChanged(c); - } catch { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'error while invoking session config callback', - }); - } - if (!c.metadata) return; // if session is marked destroyed, reset and reload @@ -1138,6 +1130,4 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { break; } } - - protected onSessionConfigChanged(_nextSessionConfig: SessionConfig): void {} } From 9fb0ac18d30a30f669e5b5f1106669feea982723 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 23:01:03 -0800 Subject: [PATCH 11/50] resolved most of the errors. yay! --- .../src/connection/WalletLinkConnection.ts | 140 ++++++++++++++++-- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 118 ++------------- 2 files changed, 140 insertions(+), 118 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 091c6c05bb..efee9d71d9 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -1,6 +1,8 @@ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 +import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; +import * as aes256gcm from '../relay/aes256gcm'; import { Session } from '../relay/Session'; import { IntNumber } from '../types'; import { @@ -31,6 +33,9 @@ import { ConnectionState, WalletLinkWebSocket } from './WalletLinkWebSocket'; const HEARTBEAT_INTERVAL = 10000; const REQUEST_TIMEOUT = 60000; +export const WALLET_USER_NAME_KEY = 'walletUsername'; +export const APP_VERSION_KEY = 'AppVersion'; + /** * Coinbase Wallet Connection */ @@ -41,7 +46,7 @@ export class WalletLinkConnection { private lastHeartbeatResponse = 0; private nextReqId = IntNumber(1); - private sessionConfigListener?: (_: SessionConfig) => void; + // private sessionConfigListener?: (_: SessionConfig) => void; setSessionConfigListener(listener: (_: SessionConfig) => void): void { this.sessionConfigListener = listener; } @@ -61,6 +66,16 @@ export class WalletLinkConnection { this.incomingEventListener = listener; } + private chainCallback: ((chainId: string, jsonRpcUrl: string) => void) | null = null; + public setChainCallback(chainCallback: (chainId: string, jsonRpcUrl: string) => void) { + this.chainCallback = chainCallback; + } + + private selectedAccountListener: ((selectedAddress: string) => void) | null = null; + public setSelectedAccountListener(selectedAccountListener: (selectedAddress: string) => void) { + this.selectedAccountListener = selectedAccountListener; + } + /** * Constructor * @param sessionId Session ID @@ -69,22 +84,22 @@ export class WalletLinkConnection { * @param [WebSocketClass] Custom WebSocket implementation */ constructor( - private sessionId: string, - private sessionKey: string, + private session: Session, linkAPIUrl: string, + private readonly storage: ScopedLocalStorage, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); this.ws = ws; - this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); + this.http = new WalletLinkHTTP(linkAPIUrl, session.id, session.key); this.ws.setConnectionStateListener(async (state) => { // attempt to reconnect every 5 seconds when disconnected this.diagnostic?.log(EVENTS.CONNECTED_STATE_CHANGE, { state, - sessionIdHash: Session.hash(sessionId), + sessionIdHash: Session.hash(session.id), }); let connected = false; @@ -156,7 +171,7 @@ export class WalletLinkConnection { case 'Linked': { const msg = m as Omit & ServerMessageLinked; this.diagnostic?.log(EVENTS.LINKED, { - sessionIdHash: Session.hash(sessionId), + sessionIdHash: Session.hash(session.id), linked: msg.linked, type: m.type, onlineGuests: msg.onlineGuests, @@ -172,7 +187,7 @@ export class WalletLinkConnection { const msg = m as Omit & ServerMessageSessionConfigUpdated; this.diagnostic?.log(EVENTS.SESSION_CONFIG_RECEIVED, { - sessionIdHash: Session.hash(sessionId), + sessionIdHash: Session.hash(session.id), metadata_keys: msg && msg.metadata ? Object.keys(msg.metadata) : undefined, }); this.sessionConfigListener?.({ @@ -204,7 +219,7 @@ export class WalletLinkConnection { throw new Error('instance is destroyed'); } this.diagnostic?.log(EVENTS.STARTED_CONNECTING, { - sessionIdHash: Session.hash(this.sessionId), + sessionIdHash: Session.hash(this.session.id), }); this.ws.connect(); } @@ -218,10 +233,10 @@ export class WalletLinkConnection { this.ws.disconnect(); this.diagnostic?.log(EVENTS.DISCONNECTED, { - sessionIdHash: Session.hash(this.sessionId), + sessionIdHash: Session.hash(this.session.id), }); - this.sessionConfigListener = undefined; + // this.sessionConfigListener = undefined; this.connectedListener = undefined; this.linkedListener = undefined; this.incomingEventListener = undefined; @@ -346,7 +361,7 @@ export class WalletLinkConnection { public async setSessionMetadata(key: string, value: string | null) { const message = ClientMessageSetSessionConfig({ id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, + sessionId: this.session.id, metadata: { [key]: value }, }); @@ -368,7 +383,7 @@ export class WalletLinkConnection { public async publishEvent(event: string, data: string, callWebhook = false) { const message = ClientMessagePublishEvent({ id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, + sessionId: this.session.id, event, data, callWebhook, @@ -433,8 +448,8 @@ export class WalletLinkConnection { private async authenticate() { const msg = ClientMessageHostSession({ id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, - sessionKey: this.sessionKey, + sessionId: this.session.id, + sessionKey: this.session.key, }); const res = await this.makeRequest(msg); if (isServerMessageFail(res)) { @@ -445,7 +460,7 @@ export class WalletLinkConnection { private sendIsLinked(): void { const msg = ClientMessageIsLinked({ id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, + sessionId: this.session.id, }); this.sendData(msg); } @@ -453,8 +468,101 @@ export class WalletLinkConnection { private sendGetSessionConfig(): void { const msg = ClientMessageGetSessionConfig({ id: IntNumber(this.nextReqId++), - sessionId: this.sessionId, + sessionId: this.session.id, }); this.sendData(msg); } + + private getSessionIdHash(): string { + return Session.hash(this.session.id); + } + + private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged + + // SessionConfigListener + private sessionConfigListener(c: SessionConfig) { + if (!c.metadata) return; + + // if session is marked destroyed, reset and reload + if (c.metadata.__destroyed === '1') { + const alreadyDestroyed = this.isDestroyed; + this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { + alreadyDestroyed, + sessionIdHash: this.getSessionIdHash(), + }); + this.resetAndReload(); // TODO!!! + } + + if (c.metadata.WalletUsername !== undefined) { + aes256gcm + .decrypt(c.metadata.WalletUsername!, this.session.secret) + .then((walletUsername) => { + this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); + }) + .catch(() => { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'username', + }); + }); + } + + if (c.metadata.AppVersion !== undefined) { + aes256gcm + .decrypt(c.metadata.AppVersion!, this.session.secret) + .then((appVersion) => { + this.storage.setItem(APP_VERSION_KEY, appVersion); + }) + .catch(() => { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'appversion', + }); + }); + } + + if (c.metadata.ChainId !== undefined && c.metadata.JsonRpcUrl !== undefined) { + Promise.all([ + aes256gcm.decrypt(c.metadata.ChainId!, this.session.secret), + aes256gcm.decrypt(c.metadata.JsonRpcUrl!, this.session.secret), + ]) + .then(([chainId, jsonRpcUrl]) => { + // custom distinctUntilChanged + if ( + this.chainCallbackParams.chainId === chainId && + this.chainCallbackParams.jsonRpcUrl === jsonRpcUrl + ) { + return; + } + this.chainCallbackParams = { + chainId, + jsonRpcUrl, + }; + + if (this.chainCallback) { + this.chainCallback(chainId, jsonRpcUrl); + } + }) + .catch(() => { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'chainId|jsonRpcUrl', + }); + }); + } + + if (c.metadata.EthereumAddress !== undefined) { + aes256gcm + .decrypt(c.metadata.EthereumAddress!, this.session.secret) + .then((selectedAddress) => { + this.selectedAccountListener?.(selectedAddress); + }) + .catch(() => { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'selectedAddress', + }); + }); + } + } } diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 0d28e73b1d..44c93228ff 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -4,7 +4,6 @@ import { DiagnosticLogger, EVENTS } from '../connection/DiagnosticLogger'; import { EventListener } from '../connection/EventListener'; import { ServerMessageEvent } from '../connection/ServerMessage'; -import { SessionConfig } from '../connection/SessionConfig'; import { WalletLinkConnection } from '../connection/WalletLinkConnection'; import { ErrorType, @@ -23,10 +22,8 @@ import { EthereumTransactionParams } from './EthereumTransactionParams'; import { RelayMessage } from './RelayMessage'; import { Session } from './Session'; import { - APP_VERSION_KEY, CancelablePromise, LOCAL_STORAGE_ADDRESSES_KEY, - WALLET_USER_NAME_KEY, WalletSDKRelayAbstract, } from './WalletSDKRelayAbstract'; import { WalletSDKRelayEventManager } from './WalletSDKRelayEventManager'; @@ -84,7 +81,6 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { protected readonly diagnostic?: DiagnosticLogger; protected connection: WalletLinkConnection; private accountsCallback: ((account: string[], isDisconnect?: boolean) => void) | null = null; - private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged private chainCallback: ((chainId: string, jsonRpcUrl: string) => void) | null = null; protected dappDefaultChain = 1; private readonly options: WalletLinkRelayOptions; @@ -137,9 +133,9 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { const session = Session.load(this.storage) || new Session(this.storage).save(); const connection = new WalletLinkConnection( - session.id, - session.key, + session, this.linkAPIUrl, + this.storage, this.diagnostic ); @@ -173,105 +169,23 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } }); - connection.setSessionConfigListener((c: SessionConfig) => { - if (!c.metadata) return; - - // if session is marked destroyed, reset and reload - if (c.metadata.__destroyed === '1') { - const alreadyDestroyed = connection.isDestroyed; - this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { - alreadyDestroyed, - sessionIdHash: this.getSessionIdHash(), - }); - this.resetAndReload(); - } - - if (c.metadata.WalletUsername !== undefined) { - aes256gcm - .decrypt(c.metadata.WalletUsername!, session.secret) - .then((walletUsername) => { - this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'username', - }); - }); + connection.setSelectedAccountListener((selectedAddress: string) => { + if (this.accountsCallback) { + this.accountsCallback([selectedAddress]); } - if (c.metadata.AppVersion !== undefined) { - aes256gcm - .decrypt(c.metadata.AppVersion!, session.secret) - .then((appVersion) => { - this.storage.setItem(APP_VERSION_KEY, appVersion); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'appversion', - }); - }); - } - - if (c.metadata.ChainId !== undefined && c.metadata.JsonRpcUrl !== undefined) { - Promise.all([ - aes256gcm.decrypt(c.metadata.ChainId!, session.secret), - aes256gcm.decrypt(c.metadata.JsonRpcUrl!, session.secret), - ]) - .then(([chainId, jsonRpcUrl]) => { - // custom distinctUntilChanged - if ( - this.chainCallbackParams.chainId === chainId && - this.chainCallbackParams.jsonRpcUrl === jsonRpcUrl - ) { - return; - } - this.chainCallbackParams = { - chainId, - jsonRpcUrl, - }; - - if (this.chainCallback) { - this.chainCallback(chainId, jsonRpcUrl); - } - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'chainId|jsonRpcUrl', - }); - }); - } - - if (c.metadata.EthereumAddress !== undefined) { - aes256gcm - .decrypt(c.metadata.EthereumAddress!, session.secret) - .then((selectedAddress) => { - if (this.accountsCallback) { - this.accountsCallback([selectedAddress]); - } - - if (WalletLinkRelay.accountRequestCallbackIds.size > 0) { - // We get the ethereum address from the metadata. If for whatever - // reason we don't get a response via an explicit web3 message - // we can still fulfill the eip1102 request. - Array.from(WalletLinkRelay.accountRequestCallbackIds.values()).forEach((id) => { - const message = Web3ResponseMessage({ - id, - response: RequestEthereumAccountsResponse([selectedAddress as AddressString]), - }); - this.invokeCallback({ ...message, id }); - }); - WalletLinkRelay.accountRequestCallbackIds.clear(); - } - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'selectedAddress', - }); + if (WalletLinkRelay.accountRequestCallbackIds.size > 0) { + // We get the ethereum address from the metadata. If for whatever + // reason we don't get a response via an explicit web3 message + // we can still fulfill the eip1102 request. + Array.from(WalletLinkRelay.accountRequestCallbackIds.values()).forEach((id) => { + const message = Web3ResponseMessage({ + id, + response: RequestEthereumAccountsResponse([selectedAddress as AddressString]), }); + this.invokeCallback({ ...message, id }); + }); + WalletLinkRelay.accountRequestCallbackIds.clear(); } }); From bb55c1966151cbf9d18f67abd259afff283ad9f1 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Fri, 10 Nov 2023 23:58:53 -0800 Subject: [PATCH 12/50] introducing Listener rename --- .../src/connection/WalletLinkConnection.ts | 60 +++------ .../wallet-sdk/src/relay/WalletLinkRelay.ts | 120 ++++++++++-------- 2 files changed, 83 insertions(+), 97 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index efee9d71d9..5a1ddd3693 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -36,6 +36,15 @@ const REQUEST_TIMEOUT = 60000; export const WALLET_USER_NAME_KEY = 'walletUsername'; export const APP_VERSION_KEY = 'AppVersion'; +export interface WalletLinkConnectionListener { + connectionLinkedUpdated: (linked: boolean) => void; + connectionConnectedUpdated: (connected: boolean) => void; + connectionIncomingEvent: (event: ServerMessageEvent) => void; + connectionChainChanged: (chainId: string, jsonRpcUrl: string) => void; + connectionAccountChanged: (selectedAddress: string) => void; + resetAndReload: () => void; +} + /** * Coinbase Wallet Connection */ @@ -46,36 +55,6 @@ export class WalletLinkConnection { private lastHeartbeatResponse = 0; private nextReqId = IntNumber(1); - // private sessionConfigListener?: (_: SessionConfig) => void; - setSessionConfigListener(listener: (_: SessionConfig) => void): void { - this.sessionConfigListener = listener; - } - - private linkedListener?: (_: boolean) => void; - setLinkedListener(listener: (_: boolean) => void): void { - this.linkedListener = listener; - } - - private connectedListener?: (_: boolean) => void; - setConnectedListener(listener: (_: boolean) => void): void { - this.connectedListener = listener; - } - - private incomingEventListener?: (_: ServerMessageEvent) => void; - setIncomingEventListener(listener: (_: ServerMessageEvent) => void): void { - this.incomingEventListener = listener; - } - - private chainCallback: ((chainId: string, jsonRpcUrl: string) => void) | null = null; - public setChainCallback(chainCallback: (chainId: string, jsonRpcUrl: string) => void) { - this.chainCallback = chainCallback; - } - - private selectedAccountListener: ((selectedAddress: string) => void) | null = null; - public setSelectedAccountListener(selectedAccountListener: (selectedAddress: string) => void) { - this.selectedAccountListener = selectedAccountListener; - } - /** * Constructor * @param sessionId Session ID @@ -84,9 +63,10 @@ export class WalletLinkConnection { * @param [WebSocketClass] Custom WebSocket implementation */ constructor( - private session: Session, linkAPIUrl: string, + private readonly session: Session, private readonly storage: ScopedLocalStorage, + private listener?: WalletLinkConnectionListener, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { @@ -237,9 +217,7 @@ export class WalletLinkConnection { }); // this.sessionConfigListener = undefined; - this.connectedListener = undefined; - this.linkedListener = undefined; - this.incomingEventListener = undefined; + this.listener = undefined; } public get isDestroyed(): boolean { @@ -257,7 +235,7 @@ export class WalletLinkConnection { private set connected(connected: boolean) { this._connected = connected; if (connected) this.onceConnected?.(); - this.connectedListener?.(connected); + this.listener?.connectionConnectedUpdated(connected); } /** @@ -288,7 +266,7 @@ export class WalletLinkConnection { private set linked(linked: boolean) { this._linked = linked; if (linked) this.onceLinked?.(); - this.linkedListener?.(linked); + this.listener?.connectionLinkedUpdated(linked); } /** @@ -326,7 +304,7 @@ export class WalletLinkConnection { return; } - this.incomingEventListener?.(m); + this.listener?.connectionIncomingEvent(m); } private shouldFetchUnseenEventsOnConnect = false; @@ -490,7 +468,7 @@ export class WalletLinkConnection { alreadyDestroyed, sessionIdHash: this.getSessionIdHash(), }); - this.resetAndReload(); // TODO!!! + this.listener?.resetAndReload(); } if (c.metadata.WalletUsername !== undefined) { @@ -539,9 +517,7 @@ export class WalletLinkConnection { jsonRpcUrl, }; - if (this.chainCallback) { - this.chainCallback(chainId, jsonRpcUrl); - } + this.listener?.connectionChainChanged(chainId, jsonRpcUrl); }) .catch(() => { this.diagnostic?.log(EVENTS.GENERAL_ERROR, { @@ -555,7 +531,7 @@ export class WalletLinkConnection { aes256gcm .decrypt(c.metadata.EthereumAddress!, this.session.secret) .then((selectedAddress) => { - this.selectedAccountListener?.(selectedAddress); + this.listener?.connectionAccountChanged(selectedAddress); }) .catch(() => { this.diagnostic?.log(EVENTS.GENERAL_ERROR, { diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 44c93228ff..d6145c637b 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -4,7 +4,10 @@ import { DiagnosticLogger, EVENTS } from '../connection/DiagnosticLogger'; import { EventListener } from '../connection/EventListener'; import { ServerMessageEvent } from '../connection/ServerMessage'; -import { WalletLinkConnection } from '../connection/WalletLinkConnection'; +import { + WalletLinkConnection, + WalletLinkConnectionListener, +} from '../connection/WalletLinkConnection'; import { ErrorType, getErrorCode, @@ -71,7 +74,10 @@ export interface WalletLinkRelayOptions { enableMobileWalletLink?: boolean; } -export class WalletLinkRelay extends WalletSDKRelayAbstract { +export class WalletLinkRelay + extends WalletSDKRelayAbstract + implements WalletLinkConnectionListener +{ private static accountRequestCallbackIds = new Set(); private readonly linkAPIUrl: string; @@ -133,78 +139,82 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { const session = Session.load(this.storage) || new Session(this.storage).save(); const connection = new WalletLinkConnection( - session, this.linkAPIUrl, + session, this.storage, + this, this.diagnostic ); - connection.setIncomingEventListener((m: ServerMessageEvent) => { - if (m.event === 'Web3Response') { - this.handleIncomingEvent(m); - } + const ui = this.options.uiConstructor({ + linkAPIUrl: this.options.linkAPIUrl, + version: this.options.version, + darkMode: this.options.darkMode, + session, }); - connection.setLinkedListener((linked: boolean) => { - this.isLinked = linked; - const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); + connection.connect(); - if (linked) { - // Only set linked session variable one way - this.session.linked = linked; - } + return { session, ui, connection }; + } - this.isUnlinkedErrorState = false; + connectionIncomingEvent(m: ServerMessageEvent) { + if (m.event === 'Web3Response') { + this.handleIncomingEvent(m); + } + } - if (cachedAddresses) { - const addresses = cachedAddresses.split(' ') as AddressString[]; - const wasConnectedViaStandalone = this.storage.getItem('IsStandaloneSigning') === 'true'; - if (addresses[0] !== '' && !linked && this.session.linked && !wasConnectedViaStandalone) { - this.isUnlinkedErrorState = true; - const sessionIdHash = this.getSessionIdHash(); - this.diagnostic?.log(EVENTS.UNLINKED_ERROR_STATE, { - sessionIdHash, - }); - } - } - }); + connectionLinkedUpdated(linked: boolean) { + this.isLinked = linked; + const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); - connection.setSelectedAccountListener((selectedAddress: string) => { - if (this.accountsCallback) { - this.accountsCallback([selectedAddress]); - } + if (linked) { + // Only set linked session variable one way + this.session.linked = linked; + } - if (WalletLinkRelay.accountRequestCallbackIds.size > 0) { - // We get the ethereum address from the metadata. If for whatever - // reason we don't get a response via an explicit web3 message - // we can still fulfill the eip1102 request. - Array.from(WalletLinkRelay.accountRequestCallbackIds.values()).forEach((id) => { - const message = Web3ResponseMessage({ - id, - response: RequestEthereumAccountsResponse([selectedAddress as AddressString]), - }); - this.invokeCallback({ ...message, id }); + this.isUnlinkedErrorState = false; + + if (cachedAddresses) { + const addresses = cachedAddresses.split(' ') as AddressString[]; + const wasConnectedViaStandalone = this.storage.getItem('IsStandaloneSigning') === 'true'; + if (addresses[0] !== '' && !linked && this.session.linked && !wasConnectedViaStandalone) { + this.isUnlinkedErrorState = true; + const sessionIdHash = this.getSessionIdHash(); + this.diagnostic?.log(EVENTS.UNLINKED_ERROR_STATE, { + sessionIdHash, }); - WalletLinkRelay.accountRequestCallbackIds.clear(); } - }); + } + } - const ui = this.options.uiConstructor({ - linkAPIUrl: this.options.linkAPIUrl, - version: this.options.version, - darkMode: this.options.darkMode, - session, - }); + connectionAccountChanged(selectedAddress: string) { + if (this.accountsCallback) { + this.accountsCallback([selectedAddress]); + } - if (ui instanceof WalletLinkRelayUI) { - connection.setConnectedListener((connected) => { - ui.setConnected(connected); + if (WalletLinkRelay.accountRequestCallbackIds.size > 0) { + // We get the ethereum address from the metadata. If for whatever + // reason we don't get a response via an explicit web3 message + // we can still fulfill the eip1102 request. + Array.from(WalletLinkRelay.accountRequestCallbackIds.values()).forEach((id) => { + const message = Web3ResponseMessage({ + id, + response: RequestEthereumAccountsResponse([selectedAddress as AddressString]), + }); + this.invokeCallback({ ...message, id }); }); + WalletLinkRelay.accountRequestCallbackIds.clear(); } + } - connection.connect(); - - return { session, ui, connection }; + connectionConnectedUpdated(connected: boolean) { + if (this.ui instanceof WalletLinkRelayUI) { + this.ui.setConnected(connected); + } + } + connectionChainChanged(chainId: string, jsonRpcUrl: string) { + this.chainCallback?.(chainId, jsonRpcUrl); } public attachUI() { From 1059b1a8d1c2d4f684548af5e2034c7e70dc8172 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 00:14:06 -0800 Subject: [PATCH 13/50] setConnected --- packages/wallet-sdk/src/provider/MobileRelayUI.ts | 2 ++ packages/wallet-sdk/src/provider/WalletUI.ts | 2 ++ packages/wallet-sdk/src/relay/WalletLinkRelay.ts | 4 +--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/wallet-sdk/src/provider/MobileRelayUI.ts b/packages/wallet-sdk/src/provider/MobileRelayUI.ts index 148bb6d479..0c0d471fbd 100644 --- a/packages/wallet-sdk/src/provider/MobileRelayUI.ts +++ b/packages/wallet-sdk/src/provider/MobileRelayUI.ts @@ -35,6 +35,8 @@ export class MobileRelayUI implements WalletUI { this.attached = true; } + setConnected(_: boolean): void {} // no-op + closeOpenedWindow() { this.openedWindow?.close(); this.openedWindow = null; diff --git a/packages/wallet-sdk/src/provider/WalletUI.ts b/packages/wallet-sdk/src/provider/WalletUI.ts index 1210ed03fa..f86c51d184 100644 --- a/packages/wallet-sdk/src/provider/WalletUI.ts +++ b/packages/wallet-sdk/src/provider/WalletUI.ts @@ -24,6 +24,8 @@ export interface WalletUIOptions { export interface WalletUI { attach(): void; + setConnected(connected: boolean): void; + /** * Opens a qr code or auth page to connect with Coinbase Wallet mobile app * @param options onCancel callback diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index d6145c637b..01fcc3ef50 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -209,9 +209,7 @@ export class WalletLinkRelay } connectionConnectedUpdated(connected: boolean) { - if (this.ui instanceof WalletLinkRelayUI) { - this.ui.setConnected(connected); - } + this.ui.setConnected(connected); } connectionChainChanged(chainId: string, jsonRpcUrl: string) { this.chainCallback?.(chainId, jsonRpcUrl); From 49edffb37834de85cb242e5d8a8a11823e9d0f67 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 00:15:20 -0800 Subject: [PATCH 14/50] update tests --- .../src/relay/WalletLinkRelay.test.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts index 0697ad5bda..0b2790e107 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts @@ -48,30 +48,19 @@ describe('WalletLinkRelay', () => { data: 'data', }; - const setIncomingEventListenerSpy = jest.spyOn( - WalletLinkConnection.prototype, - 'setIncomingEventListener' - ); - const relay = new WalletLinkRelay(options); const handleIncomingEventSpy = jest.spyOn(relay, 'handleIncomingEvent'); - expect(setIncomingEventListenerSpy).toHaveBeenCalled(); - (relay as any).connection.ws.incomingDataListener?.(serverMessageEvent); expect(handleIncomingEventSpy).toHaveBeenCalledWith(serverMessageEvent); }); it('should set isLinked with LinkedListener', async () => { - const setLinkedListenerSpy = jest.spyOn(WalletLinkConnection.prototype, 'setLinkedListener'); - const relay = new WalletLinkRelay(options); expect(relay.isLinked).toBeFalsy(); - expect(setLinkedListenerSpy).toHaveBeenCalled(); - (relay as any).connection.ws.incomingDataListener?.({ type: 'IsLinkedOK', linked: true, @@ -82,17 +71,6 @@ describe('WalletLinkRelay', () => { }); describe('setSessionConfigListener', () => { - it('should call setSessionConfigListener', async () => { - const setSessionConfigListenerSpy = jest.spyOn( - WalletLinkConnection.prototype, - 'setSessionConfigListener' - ); - - new WalletLinkRelay(options); - - expect(setSessionConfigListenerSpy).toHaveBeenCalled(); - }); - it('should update metadata with setSessionConfigListener', async () => { const sessionConfig: SessionConfig = { webhookId: 'webhookId', From ace9a7fed94f169156b8a8160b812df95f9e5e99 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 00:24:16 -0800 Subject: [PATCH 15/50] minimize change --- .../src/connection/WalletLinkConnection.ts | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 5a1ddd3693..2fcf86a3b3 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -55,6 +55,9 @@ export class WalletLinkConnection { private lastHeartbeatResponse = 0; private nextReqId = IntNumber(1); + private readonly sessionId: string; + private readonly sessionKey: string; + private readonly sessionSecret: string; /** * Constructor * @param sessionId Session ID @@ -64,22 +67,29 @@ export class WalletLinkConnection { */ constructor( linkAPIUrl: string, - private readonly session: Session, + session: Session, private readonly storage: ScopedLocalStorage, private listener?: WalletLinkConnectionListener, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { + const sessionId = session.id; + const sessionKey = session.key; + + this.sessionId = sessionId; + this.sessionKey = sessionKey; + this.sessionSecret = session.secret; + const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); this.ws = ws; - this.http = new WalletLinkHTTP(linkAPIUrl, session.id, session.key); + this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); this.ws.setConnectionStateListener(async (state) => { // attempt to reconnect every 5 seconds when disconnected this.diagnostic?.log(EVENTS.CONNECTED_STATE_CHANGE, { state, - sessionIdHash: Session.hash(session.id), + sessionIdHash: Session.hash(sessionId), }); let connected = false; @@ -151,7 +161,7 @@ export class WalletLinkConnection { case 'Linked': { const msg = m as Omit & ServerMessageLinked; this.diagnostic?.log(EVENTS.LINKED, { - sessionIdHash: Session.hash(session.id), + sessionIdHash: Session.hash(sessionId), linked: msg.linked, type: m.type, onlineGuests: msg.onlineGuests, @@ -167,7 +177,7 @@ export class WalletLinkConnection { const msg = m as Omit & ServerMessageSessionConfigUpdated; this.diagnostic?.log(EVENTS.SESSION_CONFIG_RECEIVED, { - sessionIdHash: Session.hash(session.id), + sessionIdHash: Session.hash(sessionId), metadata_keys: msg && msg.metadata ? Object.keys(msg.metadata) : undefined, }); this.sessionConfigListener?.({ @@ -199,7 +209,7 @@ export class WalletLinkConnection { throw new Error('instance is destroyed'); } this.diagnostic?.log(EVENTS.STARTED_CONNECTING, { - sessionIdHash: Session.hash(this.session.id), + sessionIdHash: Session.hash(this.sessionId), }); this.ws.connect(); } @@ -213,7 +223,7 @@ export class WalletLinkConnection { this.ws.disconnect(); this.diagnostic?.log(EVENTS.DISCONNECTED, { - sessionIdHash: Session.hash(this.session.id), + sessionIdHash: Session.hash(this.sessionId), }); // this.sessionConfigListener = undefined; @@ -339,7 +349,7 @@ export class WalletLinkConnection { public async setSessionMetadata(key: string, value: string | null) { const message = ClientMessageSetSessionConfig({ id: IntNumber(this.nextReqId++), - sessionId: this.session.id, + sessionId: this.sessionId, metadata: { [key]: value }, }); @@ -361,7 +371,7 @@ export class WalletLinkConnection { public async publishEvent(event: string, data: string, callWebhook = false) { const message = ClientMessagePublishEvent({ id: IntNumber(this.nextReqId++), - sessionId: this.session.id, + sessionId: this.sessionId, event, data, callWebhook, @@ -426,8 +436,8 @@ export class WalletLinkConnection { private async authenticate() { const msg = ClientMessageHostSession({ id: IntNumber(this.nextReqId++), - sessionId: this.session.id, - sessionKey: this.session.key, + sessionId: this.sessionId, + sessionKey: this.sessionKey, }); const res = await this.makeRequest(msg); if (isServerMessageFail(res)) { @@ -438,7 +448,7 @@ export class WalletLinkConnection { private sendIsLinked(): void { const msg = ClientMessageIsLinked({ id: IntNumber(this.nextReqId++), - sessionId: this.session.id, + sessionId: this.sessionId, }); this.sendData(msg); } @@ -446,13 +456,13 @@ export class WalletLinkConnection { private sendGetSessionConfig(): void { const msg = ClientMessageGetSessionConfig({ id: IntNumber(this.nextReqId++), - sessionId: this.session.id, + sessionId: this.sessionId, }); this.sendData(msg); } private getSessionIdHash(): string { - return Session.hash(this.session.id); + return Session.hash(this.sessionId); } private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged @@ -473,7 +483,7 @@ export class WalletLinkConnection { if (c.metadata.WalletUsername !== undefined) { aes256gcm - .decrypt(c.metadata.WalletUsername!, this.session.secret) + .decrypt(c.metadata.WalletUsername!, this.sessionSecret) .then((walletUsername) => { this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); }) @@ -487,7 +497,7 @@ export class WalletLinkConnection { if (c.metadata.AppVersion !== undefined) { aes256gcm - .decrypt(c.metadata.AppVersion!, this.session.secret) + .decrypt(c.metadata.AppVersion!, this.sessionSecret) .then((appVersion) => { this.storage.setItem(APP_VERSION_KEY, appVersion); }) @@ -501,8 +511,8 @@ export class WalletLinkConnection { if (c.metadata.ChainId !== undefined && c.metadata.JsonRpcUrl !== undefined) { Promise.all([ - aes256gcm.decrypt(c.metadata.ChainId!, this.session.secret), - aes256gcm.decrypt(c.metadata.JsonRpcUrl!, this.session.secret), + aes256gcm.decrypt(c.metadata.ChainId!, this.sessionSecret), + aes256gcm.decrypt(c.metadata.JsonRpcUrl!, this.sessionSecret), ]) .then(([chainId, jsonRpcUrl]) => { // custom distinctUntilChanged @@ -529,7 +539,7 @@ export class WalletLinkConnection { if (c.metadata.EthereumAddress !== undefined) { aes256gcm - .decrypt(c.metadata.EthereumAddress!, this.session.secret) + .decrypt(c.metadata.EthereumAddress!, this.sessionSecret) .then((selectedAddress) => { this.listener?.connectionAccountChanged(selectedAddress); }) From eec294af22825de230a7a55da9681607c9c92288 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 00:37:17 -0800 Subject: [PATCH 16/50] introduce cipher --- .../src/connection/WalletLinkConnection.ts | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 2fcf86a3b3..9529dcbcc9 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -57,7 +57,7 @@ export class WalletLinkConnection { private readonly sessionId: string; private readonly sessionKey: string; - private readonly sessionSecret: string; + private readonly cipher: WalletLinkConnectionCipher; /** * Constructor * @param sessionId Session ID @@ -73,12 +73,10 @@ export class WalletLinkConnection { private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { - const sessionId = session.id; - const sessionKey = session.key; + const sessionId = (this.sessionId = session.id); + const sessionKey = (this.sessionKey = session.key); - this.sessionId = sessionId; - this.sessionKey = sessionKey; - this.sessionSecret = session.secret; + this.cipher = new WalletLinkConnectionCipher(session.secret); const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); this.ws = ws; @@ -482,8 +480,9 @@ export class WalletLinkConnection { } if (c.metadata.WalletUsername !== undefined) { - aes256gcm - .decrypt(c.metadata.WalletUsername!, this.sessionSecret) + aes256gcm; + this.cipher + .decrypt(c.metadata.WalletUsername!) .then((walletUsername) => { this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); }) @@ -496,8 +495,8 @@ export class WalletLinkConnection { } if (c.metadata.AppVersion !== undefined) { - aes256gcm - .decrypt(c.metadata.AppVersion!, this.sessionSecret) + this.cipher + .decrypt(c.metadata.AppVersion!) .then((appVersion) => { this.storage.setItem(APP_VERSION_KEY, appVersion); }) @@ -511,8 +510,8 @@ export class WalletLinkConnection { if (c.metadata.ChainId !== undefined && c.metadata.JsonRpcUrl !== undefined) { Promise.all([ - aes256gcm.decrypt(c.metadata.ChainId!, this.sessionSecret), - aes256gcm.decrypt(c.metadata.JsonRpcUrl!, this.sessionSecret), + this.cipher.decrypt(c.metadata.ChainId!), + this.cipher.decrypt(c.metadata.JsonRpcUrl!), ]) .then(([chainId, jsonRpcUrl]) => { // custom distinctUntilChanged @@ -538,8 +537,8 @@ export class WalletLinkConnection { } if (c.metadata.EthereumAddress !== undefined) { - aes256gcm - .decrypt(c.metadata.EthereumAddress!, this.sessionSecret) + this.cipher + .decrypt(c.metadata.EthereumAddress!) .then((selectedAddress) => { this.listener?.connectionAccountChanged(selectedAddress); }) @@ -552,3 +551,11 @@ export class WalletLinkConnection { } } } + +class WalletLinkConnectionCipher { + constructor(private readonly secret: string) {} + + decrypt(cipherText: string): Promise { + return aes256gcm.decrypt(cipherText, this.secret); + } +} From 932031272e492014ec72e8ec36e3bf5518cc853d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 00:52:36 -0800 Subject: [PATCH 17/50] handle metadata --- .../src/connection/WalletLinkConnection.ts | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 9529dcbcc9..64722a323f 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -463,6 +463,20 @@ export class WalletLinkConnection { return Session.hash(this.sessionId); } + private handleMetadata(key: string, metadataValue: string) { + this.cipher + .decrypt(metadataValue) + .then((decryptedValue) => { + this.storage.setItem(key, decryptedValue); + }) + .catch(() => { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: key, + }); + }); + } + private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged // SessionConfigListener @@ -479,33 +493,14 @@ export class WalletLinkConnection { this.listener?.resetAndReload(); } - if (c.metadata.WalletUsername !== undefined) { - aes256gcm; - this.cipher - .decrypt(c.metadata.WalletUsername!) - .then((walletUsername) => { - this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'username', - }); - }); + const { WalletUsername, AppVersion } = c.metadata; + + if (WalletUsername !== undefined) { + this.handleMetadata(WALLET_USER_NAME_KEY, WalletUsername); } - if (c.metadata.AppVersion !== undefined) { - this.cipher - .decrypt(c.metadata.AppVersion!) - .then((appVersion) => { - this.storage.setItem(APP_VERSION_KEY, appVersion); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'appversion', - }); - }); + if (AppVersion !== undefined) { + this.handleMetadata(APP_VERSION_KEY, AppVersion); } if (c.metadata.ChainId !== undefined && c.metadata.JsonRpcUrl !== undefined) { From 52734c2f2bedc6472bcbb367145e934a1968b8d5 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 00:58:40 -0800 Subject: [PATCH 18/50] unwrap --- .../src/connection/WalletLinkConnection.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 64722a323f..a34f4359e6 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -480,11 +480,14 @@ export class WalletLinkConnection { private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged // SessionConfigListener - private sessionConfigListener(c: SessionConfig) { - if (!c.metadata) return; + private sessionConfigListener(sessionConfig: SessionConfig) { + const { metadata } = sessionConfig; + if (!metadata) return; - // if session is marked destroyed, reset and reload - if (c.metadata.__destroyed === '1') { + const { __destroyed, WalletUsername, AppVersion, ChainId, JsonRpcUrl, EthereumAddress } = + metadata; + + if (__destroyed === '1') { const alreadyDestroyed = this.isDestroyed; this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { alreadyDestroyed, @@ -493,8 +496,6 @@ export class WalletLinkConnection { this.listener?.resetAndReload(); } - const { WalletUsername, AppVersion } = c.metadata; - if (WalletUsername !== undefined) { this.handleMetadata(WALLET_USER_NAME_KEY, WalletUsername); } @@ -503,11 +504,8 @@ export class WalletLinkConnection { this.handleMetadata(APP_VERSION_KEY, AppVersion); } - if (c.metadata.ChainId !== undefined && c.metadata.JsonRpcUrl !== undefined) { - Promise.all([ - this.cipher.decrypt(c.metadata.ChainId!), - this.cipher.decrypt(c.metadata.JsonRpcUrl!), - ]) + if (ChainId !== undefined && JsonRpcUrl !== undefined) { + Promise.all([this.cipher.decrypt(ChainId), this.cipher.decrypt(JsonRpcUrl)]) .then(([chainId, jsonRpcUrl]) => { // custom distinctUntilChanged if ( @@ -531,9 +529,9 @@ export class WalletLinkConnection { }); } - if (c.metadata.EthereumAddress !== undefined) { + if (EthereumAddress !== undefined) { this.cipher - .decrypt(c.metadata.EthereumAddress!) + .decrypt(EthereumAddress) .then((selectedAddress) => { this.listener?.connectionAccountChanged(selectedAddress); }) From 16eded0e5726bde146997331f38f38b886c3727d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:01:12 -0800 Subject: [PATCH 19/50] cleanup __destroyed --- .../wallet-sdk/src/connection/WalletLinkConnection.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index a34f4359e6..2e8e472a81 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -459,10 +459,6 @@ export class WalletLinkConnection { this.sendData(msg); } - private getSessionIdHash(): string { - return Session.hash(this.sessionId); - } - private handleMetadata(key: string, metadataValue: string) { this.cipher .decrypt(metadataValue) @@ -488,12 +484,11 @@ export class WalletLinkConnection { metadata; if (__destroyed === '1') { - const alreadyDestroyed = this.isDestroyed; + this.listener?.resetAndReload(); this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { - alreadyDestroyed, - sessionIdHash: this.getSessionIdHash(), + alreadyDestroyed: this.isDestroyed, + sessionIdHash: Session.hash(this.sessionId), }); - this.listener?.resetAndReload(); } if (WalletUsername !== undefined) { From 1fafe2f8f856d2bf68d68ae802ffa69e47a64153 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:03:58 -0800 Subject: [PATCH 20/50] WIP handleChainChanged --- .../src/connection/WalletLinkConnection.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 2e8e472a81..e77972a0d8 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -473,6 +473,22 @@ export class WalletLinkConnection { }); } + private handleChainChanged(chainId: string, jsonRpcUrl: string) { + // custom distinctUntilChanged + if ( + this.chainCallbackParams.chainId === chainId && + this.chainCallbackParams.jsonRpcUrl === jsonRpcUrl + ) { + return; + } + this.chainCallbackParams = { + chainId, + jsonRpcUrl, + }; + + this.listener?.connectionChainChanged(chainId, jsonRpcUrl); + } + private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged // SessionConfigListener @@ -501,21 +517,7 @@ export class WalletLinkConnection { if (ChainId !== undefined && JsonRpcUrl !== undefined) { Promise.all([this.cipher.decrypt(ChainId), this.cipher.decrypt(JsonRpcUrl)]) - .then(([chainId, jsonRpcUrl]) => { - // custom distinctUntilChanged - if ( - this.chainCallbackParams.chainId === chainId && - this.chainCallbackParams.jsonRpcUrl === jsonRpcUrl - ) { - return; - } - this.chainCallbackParams = { - chainId, - jsonRpcUrl, - }; - - this.listener?.connectionChainChanged(chainId, jsonRpcUrl); - }) + .then(([chainId, jsonRpcUrl]) => {}) .catch(() => { this.diagnostic?.log(EVENTS.GENERAL_ERROR, { message: 'Had error decrypting', From 08f73c040c26b8a2334e1e5177a324cd057921a5 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:08:16 -0800 Subject: [PATCH 21/50] cleanup handleMetadata --- .../src/connection/WalletLinkConnection.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index e77972a0d8..ab4904607e 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -459,18 +459,9 @@ export class WalletLinkConnection { this.sendData(msg); } - private handleMetadata(key: string, metadataValue: string) { - this.cipher - .decrypt(metadataValue) - .then((decryptedValue) => { - this.storage.setItem(key, decryptedValue); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: key, - }); - }); + private async handleMetadataChanged(key: string, metadataValue: string) { + const decryptedValue = await this.cipher.decrypt(metadataValue); + this.storage.setItem(key, decryptedValue); } private handleChainChanged(chainId: string, jsonRpcUrl: string) { @@ -508,11 +499,11 @@ export class WalletLinkConnection { } if (WalletUsername !== undefined) { - this.handleMetadata(WALLET_USER_NAME_KEY, WalletUsername); + this.handleMetadataChanged(WALLET_USER_NAME_KEY, WalletUsername); } if (AppVersion !== undefined) { - this.handleMetadata(APP_VERSION_KEY, AppVersion); + this.handleMetadataChanged(APP_VERSION_KEY, AppVersion); } if (ChainId !== undefined && JsonRpcUrl !== undefined) { From e0f3af2351602e1cc2ae7348a5f826708095328e Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:21:28 -0800 Subject: [PATCH 22/50] handleChainChanged --- .../src/connection/WalletLinkConnection.ts | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index ab4904607e..ed3ace8073 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -464,23 +464,19 @@ export class WalletLinkConnection { this.storage.setItem(key, decryptedValue); } - private handleChainChanged(chainId: string, jsonRpcUrl: string) { - // custom distinctUntilChanged - if ( - this.chainCallbackParams.chainId === chainId && - this.chainCallbackParams.jsonRpcUrl === jsonRpcUrl - ) { - return; - } - this.chainCallbackParams = { - chainId, - jsonRpcUrl, - }; + private async handleChainChanged(encryptedChainId: string, encryptedJsonRpcUrl: string) { + const chainId = await this.cipher.decrypt(encryptedChainId); + const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); + + if (this.chainId === chainId && this.jsonRpcUrl === jsonRpcUrl) return; + this.chainId = chainId; + this.jsonRpcUrl = jsonRpcUrl; this.listener?.connectionChainChanged(chainId, jsonRpcUrl); } - private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged + private chainId = ''; + private jsonRpcUrl = ''; // SessionConfigListener private sessionConfigListener(sessionConfig: SessionConfig) { @@ -497,26 +493,15 @@ export class WalletLinkConnection { sessionIdHash: Session.hash(this.sessionId), }); } - if (WalletUsername !== undefined) { this.handleMetadataChanged(WALLET_USER_NAME_KEY, WalletUsername); } - if (AppVersion !== undefined) { this.handleMetadataChanged(APP_VERSION_KEY, AppVersion); } - if (ChainId !== undefined && JsonRpcUrl !== undefined) { - Promise.all([this.cipher.decrypt(ChainId), this.cipher.decrypt(JsonRpcUrl)]) - .then(([chainId, jsonRpcUrl]) => {}) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'chainId|jsonRpcUrl', - }); - }); + this.handleChainChanged(ChainId, JsonRpcUrl); } - if (EthereumAddress !== undefined) { this.cipher .decrypt(EthereumAddress) From 82d231c0647c190c1578697e66bd798edf4de4e8 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:29:13 -0800 Subject: [PATCH 23/50] handleAccountChanged + cleanup --- packages/wallet-sdk/.eslintrc.js | 3 + .../src/connection/WalletLinkConnection.ts | 67 ++++++++----------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/wallet-sdk/.eslintrc.js b/packages/wallet-sdk/.eslintrc.js index ff45469828..ab64bfe3df 100644 --- a/packages/wallet-sdk/.eslintrc.js +++ b/packages/wallet-sdk/.eslintrc.js @@ -5,4 +5,7 @@ module.exports = { pragma: "h", }, }, + rules: { + 'no-useless-constructor': 'off', + }, }; diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index ed3ace8073..2defb099d8 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -58,13 +58,10 @@ export class WalletLinkConnection { private readonly sessionId: string; private readonly sessionKey: string; private readonly cipher: WalletLinkConnectionCipher; - /** - * Constructor - * @param sessionId Session ID - * @param sessionKey Session Key - * @param linkAPIUrl Coinbase Wallet link server URL - * @param [WebSocketClass] Custom WebSocket implementation - */ + + private chainId = ''; + private jsonRpcUrl = ''; + constructor( linkAPIUrl: string, session: Session, @@ -224,7 +221,6 @@ export class WalletLinkConnection { sessionIdHash: Session.hash(this.sessionId), }); - // this.sessionConfigListener = undefined; this.listener = undefined; } @@ -459,26 +455,8 @@ export class WalletLinkConnection { this.sendData(msg); } - private async handleMetadataChanged(key: string, metadataValue: string) { - const decryptedValue = await this.cipher.decrypt(metadataValue); - this.storage.setItem(key, decryptedValue); - } - - private async handleChainChanged(encryptedChainId: string, encryptedJsonRpcUrl: string) { - const chainId = await this.cipher.decrypt(encryptedChainId); - const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); - - if (this.chainId === chainId && this.jsonRpcUrl === jsonRpcUrl) return; - - this.chainId = chainId; - this.jsonRpcUrl = jsonRpcUrl; - this.listener?.connectionChainChanged(chainId, jsonRpcUrl); - } - - private chainId = ''; - private jsonRpcUrl = ''; - // SessionConfigListener + private sessionConfigListener(sessionConfig: SessionConfig) { const { metadata } = sessionConfig; if (!metadata) return; @@ -503,25 +481,36 @@ export class WalletLinkConnection { this.handleChainChanged(ChainId, JsonRpcUrl); } if (EthereumAddress !== undefined) { - this.cipher - .decrypt(EthereumAddress) - .then((selectedAddress) => { - this.listener?.connectionAccountChanged(selectedAddress); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'selectedAddress', - }); - }); + this.handleAccountChanged(EthereumAddress); } } + + private async handleMetadataChanged(key: string, metadataValue: string) { + const decryptedValue = await this.cipher.decrypt(metadataValue); + this.storage.setItem(key, decryptedValue); + } + + private async handleChainChanged(encryptedChainId: string, encryptedJsonRpcUrl: string) { + const chainId = await this.cipher.decrypt(encryptedChainId); + const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); + + if (this.chainId === chainId && this.jsonRpcUrl === jsonRpcUrl) return; + + this.chainId = chainId; + this.jsonRpcUrl = jsonRpcUrl; + this.listener?.connectionChainChanged(chainId, jsonRpcUrl); + } + + private async handleAccountChanged(encryptedSelectedAddress: string) { + const selectedAddress = await this.cipher.decrypt(encryptedSelectedAddress); + this.listener?.connectionAccountChanged(selectedAddress); + } } class WalletLinkConnectionCipher { constructor(private readonly secret: string) {} - decrypt(cipherText: string): Promise { + async decrypt(cipherText: string): Promise { return aes256gcm.decrypt(cipherText, this.secret); } } From 9563dba115ae8f14c5f01facd7e02beea19539b2 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:52:21 -0800 Subject: [PATCH 24/50] remove storage dependency --- .../src/connection/WalletLinkConnection.ts | 9 ++++----- packages/wallet-sdk/src/relay/WalletLinkRelay.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 2defb099d8..6d920e40f3 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -1,7 +1,6 @@ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 -import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; import * as aes256gcm from '../relay/aes256gcm'; import { Session } from '../relay/Session'; import { IntNumber } from '../types'; @@ -42,7 +41,8 @@ export interface WalletLinkConnectionListener { connectionIncomingEvent: (event: ServerMessageEvent) => void; connectionChainChanged: (chainId: string, jsonRpcUrl: string) => void; connectionAccountChanged: (selectedAddress: string) => void; - resetAndReload: () => void; + connectionMetadataChanged: (key: string, metadataValue: string) => void; + connectionResetAndReload: () => void; } /** @@ -65,7 +65,6 @@ export class WalletLinkConnection { constructor( linkAPIUrl: string, session: Session, - private readonly storage: ScopedLocalStorage, private listener?: WalletLinkConnectionListener, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket @@ -465,7 +464,7 @@ export class WalletLinkConnection { metadata; if (__destroyed === '1') { - this.listener?.resetAndReload(); + this.listener?.connectionResetAndReload(); this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { alreadyDestroyed: this.isDestroyed, sessionIdHash: Session.hash(this.sessionId), @@ -487,7 +486,7 @@ export class WalletLinkConnection { private async handleMetadataChanged(key: string, metadataValue: string) { const decryptedValue = await this.cipher.decrypt(metadataValue); - this.storage.setItem(key, decryptedValue); + this.listener?.connectionMetadataChanged(key, decryptedValue); } private async handleChainChanged(encryptedChainId: string, encryptedJsonRpcUrl: string) { diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 01fcc3ef50..d6bac5a4f9 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -138,13 +138,7 @@ export class WalletLinkRelay public subscribe() { const session = Session.load(this.storage) || new Session(this.storage).save(); - const connection = new WalletLinkConnection( - this.linkAPIUrl, - session, - this.storage, - this, - this.diagnostic - ); + const connection = new WalletLinkConnection(this.linkAPIUrl, session, this, this.diagnostic); const ui = this.options.uiConstructor({ linkAPIUrl: this.options.linkAPIUrl, @@ -158,6 +152,10 @@ export class WalletLinkRelay return { session, ui, connection }; } + connectionResetAndReload() { + this.resetAndReload(); + } + connectionIncomingEvent(m: ServerMessageEvent) { if (m.event === 'Web3Response') { this.handleIncomingEvent(m); @@ -208,6 +206,10 @@ export class WalletLinkRelay } } + connectionMetadataChanged(key: string, metadataValue: string) { + this.storage.setItem(key, metadataValue); + } + connectionConnectedUpdated(connected: boolean) { this.ui.setConnected(connected); } From d3ef3a74023b335ab1e13ca3f89126e71befc502 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 01:58:01 -0800 Subject: [PATCH 25/50] cleanup --- .../src/connection/WalletLinkConnection.ts | 2 +- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 120 ++++++++---------- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 6d920e40f3..b0c94fcf41 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -63,8 +63,8 @@ export class WalletLinkConnection { private jsonRpcUrl = ''; constructor( - linkAPIUrl: string, session: Session, + linkAPIUrl: string, private listener?: WalletLinkConnectionListener, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index d6bac5a4f9..570a4003dc 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -74,10 +74,7 @@ export interface WalletLinkRelayOptions { enableMobileWalletLink?: boolean; } -export class WalletLinkRelay - extends WalletSDKRelayAbstract - implements WalletLinkConnectionListener -{ +export class WalletLinkRelay extends WalletSDKRelayAbstract { private static accountRequestCallbackIds = new Set(); private readonly linkAPIUrl: string; @@ -138,7 +135,12 @@ export class WalletLinkRelay public subscribe() { const session = Session.load(this.storage) || new Session(this.storage).save(); - const connection = new WalletLinkConnection(this.linkAPIUrl, session, this, this.diagnostic); + const connection = new WalletLinkConnection( + session, + this.linkAPIUrl, + this.connectionListener, + this.diagnostic + ); const ui = this.options.uiConstructor({ linkAPIUrl: this.options.linkAPIUrl, @@ -152,70 +154,60 @@ export class WalletLinkRelay return { session, ui, connection }; } - connectionResetAndReload() { - this.resetAndReload(); - } - - connectionIncomingEvent(m: ServerMessageEvent) { - if (m.event === 'Web3Response') { - this.handleIncomingEvent(m); - } - } - - connectionLinkedUpdated(linked: boolean) { - this.isLinked = linked; - const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); - - if (linked) { - // Only set linked session variable one way - this.session.linked = linked; - } + private connectionListener: WalletLinkConnectionListener = { + connectionIncomingEvent: (m: ServerMessageEvent) => { + if (m.event === 'Web3Response') { + this.handleIncomingEvent(m); + } + }, + connectionLinkedUpdated: (linked: boolean) => { + this.isLinked = linked; + const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); + + if (linked) { + // Only set linked session variable one way + this.session.linked = linked; + } - this.isUnlinkedErrorState = false; + this.isUnlinkedErrorState = false; - if (cachedAddresses) { - const addresses = cachedAddresses.split(' ') as AddressString[]; - const wasConnectedViaStandalone = this.storage.getItem('IsStandaloneSigning') === 'true'; - if (addresses[0] !== '' && !linked && this.session.linked && !wasConnectedViaStandalone) { - this.isUnlinkedErrorState = true; - const sessionIdHash = this.getSessionIdHash(); - this.diagnostic?.log(EVENTS.UNLINKED_ERROR_STATE, { - sessionIdHash, - }); + if (cachedAddresses) { + const addresses = cachedAddresses.split(' ') as AddressString[]; + const wasConnectedViaStandalone = this.storage.getItem('IsStandaloneSigning') === 'true'; + if (addresses[0] !== '' && !linked && this.session.linked && !wasConnectedViaStandalone) { + this.isUnlinkedErrorState = true; + const sessionIdHash = this.getSessionIdHash(); + this.diagnostic?.log(EVENTS.UNLINKED_ERROR_STATE, { + sessionIdHash, + }); + } + } + }, + connectionMetadataChanged: (key: string, value: string) => this.storage.setItem(key, value), + connectionAccountChanged: (selectedAddress: string) => { + if (this.accountsCallback) { + this.accountsCallback([selectedAddress]); } - } - } - - connectionAccountChanged(selectedAddress: string) { - if (this.accountsCallback) { - this.accountsCallback([selectedAddress]); - } - if (WalletLinkRelay.accountRequestCallbackIds.size > 0) { - // We get the ethereum address from the metadata. If for whatever - // reason we don't get a response via an explicit web3 message - // we can still fulfill the eip1102 request. - Array.from(WalletLinkRelay.accountRequestCallbackIds.values()).forEach((id) => { - const message = Web3ResponseMessage({ - id, - response: RequestEthereumAccountsResponse([selectedAddress as AddressString]), + if (WalletLinkRelay.accountRequestCallbackIds.size > 0) { + // We get the ethereum address from the metadata. If for whatever + // reason we don't get a response via an explicit web3 message + // we can still fulfill the eip1102 request. + Array.from(WalletLinkRelay.accountRequestCallbackIds.values()).forEach((id) => { + const message = Web3ResponseMessage({ + id, + response: RequestEthereumAccountsResponse([selectedAddress as AddressString]), + }); + this.invokeCallback({ ...message, id }); }); - this.invokeCallback({ ...message, id }); - }); - WalletLinkRelay.accountRequestCallbackIds.clear(); - } - } - - connectionMetadataChanged(key: string, metadataValue: string) { - this.storage.setItem(key, metadataValue); - } - - connectionConnectedUpdated(connected: boolean) { - this.ui.setConnected(connected); - } - connectionChainChanged(chainId: string, jsonRpcUrl: string) { - this.chainCallback?.(chainId, jsonRpcUrl); - } + WalletLinkRelay.accountRequestCallbackIds.clear(); + } + }, + connectionConnectedUpdated: (connected: boolean) => this.ui.setConnected(connected), + connectionChainChanged: (chainId: string, rpcUrl: string) => + this.chainCallback?.(chainId, rpcUrl), + connectionResetAndReload: () => this.resetAndReload(), + }; public attachUI() { this.ui.attach(); From 7233426f632945b0c62e956b6ff17ea47eb2cc5f Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 02:36:59 -0800 Subject: [PATCH 26/50] handleMetadataUpdated --- .../src/connection/WalletLinkConnection.ts | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index b0c94fcf41..938fbf9fa5 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -57,8 +57,7 @@ export class WalletLinkConnection { private readonly sessionId: string; private readonly sessionKey: string; - private readonly cipher: WalletLinkConnectionCipher; - + private readonly decrypt: (_: string) => Promise; private chainId = ''; private jsonRpcUrl = ''; @@ -72,7 +71,7 @@ export class WalletLinkConnection { const sessionId = (this.sessionId = session.id); const sessionKey = (this.sessionKey = session.key); - this.cipher = new WalletLinkConnectionCipher(session.secret); + this.decrypt = (cipherText: string) => aes256gcm.decrypt(cipherText, session.secret); const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); this.ws = ws; @@ -174,11 +173,9 @@ export class WalletLinkConnection { sessionIdHash: Session.hash(sessionId), metadata_keys: msg && msg.metadata ? Object.keys(msg.metadata) : undefined, }); - this.sessionConfigListener?.({ - webhookId: msg.webhookId, - webhookUrl: msg.webhookUrl, - metadata: msg.metadata, - }); + if (msg.metadata) { + this.handleMetadataUpdated(msg.metadata); + } break; } @@ -454,21 +451,12 @@ export class WalletLinkConnection { this.sendData(msg); } - // SessionConfigListener - - private sessionConfigListener(sessionConfig: SessionConfig) { - const { metadata } = sessionConfig; - if (!metadata) return; - + private handleMetadataUpdated(metadata: SessionConfig['metadata']) { const { __destroyed, WalletUsername, AppVersion, ChainId, JsonRpcUrl, EthereumAddress } = metadata; if (__destroyed === '1') { - this.listener?.connectionResetAndReload(); - this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { - alreadyDestroyed: this.isDestroyed, - sessionIdHash: Session.hash(this.sessionId), - }); + this.handleDestroyed(); } if (WalletUsername !== undefined) { this.handleMetadataChanged(WALLET_USER_NAME_KEY, WalletUsername); @@ -484,14 +472,22 @@ export class WalletLinkConnection { } } + private handleDestroyed() { + this.listener?.connectionResetAndReload(); + this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { + alreadyDestroyed: this.isDestroyed, + sessionIdHash: Session.hash(this.sessionId), + }); + } + private async handleMetadataChanged(key: string, metadataValue: string) { - const decryptedValue = await this.cipher.decrypt(metadataValue); + const decryptedValue = await this.decrypt(metadataValue); this.listener?.connectionMetadataChanged(key, decryptedValue); } private async handleChainChanged(encryptedChainId: string, encryptedJsonRpcUrl: string) { - const chainId = await this.cipher.decrypt(encryptedChainId); - const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); + const chainId = await this.decrypt(encryptedChainId); + const jsonRpcUrl = await this.decrypt(encryptedJsonRpcUrl); if (this.chainId === chainId && this.jsonRpcUrl === jsonRpcUrl) return; @@ -501,15 +497,7 @@ export class WalletLinkConnection { } private async handleAccountChanged(encryptedSelectedAddress: string) { - const selectedAddress = await this.cipher.decrypt(encryptedSelectedAddress); + const selectedAddress = await this.decrypt(encryptedSelectedAddress); this.listener?.connectionAccountChanged(selectedAddress); } } - -class WalletLinkConnectionCipher { - constructor(private readonly secret: string) {} - - async decrypt(cipherText: string): Promise { - return aes256gcm.decrypt(cipherText, this.secret); - } -} From ed2bc87f1a52e0e64be13dc8fa8601069921b85e Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 02:45:59 -0800 Subject: [PATCH 27/50] rename --- .../src/connection/WalletLinkConnection.ts | 50 +++++++++---------- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 21 ++++---- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 938fbf9fa5..e508fb94a5 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -35,14 +35,14 @@ const REQUEST_TIMEOUT = 60000; export const WALLET_USER_NAME_KEY = 'walletUsername'; export const APP_VERSION_KEY = 'AppVersion'; -export interface WalletLinkConnectionListener { - connectionLinkedUpdated: (linked: boolean) => void; - connectionConnectedUpdated: (connected: boolean) => void; - connectionIncomingEvent: (event: ServerMessageEvent) => void; - connectionChainChanged: (chainId: string, jsonRpcUrl: string) => void; - connectionAccountChanged: (selectedAddress: string) => void; - connectionMetadataChanged: (key: string, metadataValue: string) => void; - connectionResetAndReload: () => void; +export interface WalletLinkConnectionUpdateListener { + linkedUpdated: (linked: boolean) => void; + connectedUpdated: (connected: boolean) => void; + incomingEvent: (event: ServerMessageEvent) => void; + chainUpdated: (chainId: string, jsonRpcUrl: string) => void; + accountUpdated: (selectedAddress: string) => void; + metadataUpdated: (key: string, metadataValue: string) => void; + resetAndReload: () => void; } /** @@ -64,7 +64,7 @@ export class WalletLinkConnection { constructor( session: Session, linkAPIUrl: string, - private listener?: WalletLinkConnectionListener, + private listener?: WalletLinkConnectionUpdateListener, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { @@ -174,7 +174,7 @@ export class WalletLinkConnection { metadata_keys: msg && msg.metadata ? Object.keys(msg.metadata) : undefined, }); if (msg.metadata) { - this.handleMetadataUpdated(msg.metadata); + this.handleSessionConfigUpdated(msg.metadata); } break; } @@ -235,7 +235,7 @@ export class WalletLinkConnection { private set connected(connected: boolean) { this._connected = connected; if (connected) this.onceConnected?.(); - this.listener?.connectionConnectedUpdated(connected); + this.listener?.connectedUpdated(connected); } /** @@ -266,7 +266,7 @@ export class WalletLinkConnection { private set linked(linked: boolean) { this._linked = linked; if (linked) this.onceLinked?.(); - this.listener?.connectionLinkedUpdated(linked); + this.listener?.linkedUpdated(linked); } /** @@ -304,7 +304,7 @@ export class WalletLinkConnection { return; } - this.listener?.connectionIncomingEvent(m); + this.listener?.incomingEvent(m); } private shouldFetchUnseenEventsOnConnect = false; @@ -451,7 +451,7 @@ export class WalletLinkConnection { this.sendData(msg); } - private handleMetadataUpdated(metadata: SessionConfig['metadata']) { + private handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { const { __destroyed, WalletUsername, AppVersion, ChainId, JsonRpcUrl, EthereumAddress } = metadata; @@ -459,33 +459,33 @@ export class WalletLinkConnection { this.handleDestroyed(); } if (WalletUsername !== undefined) { - this.handleMetadataChanged(WALLET_USER_NAME_KEY, WalletUsername); + this.handleMetadataUpdated(WALLET_USER_NAME_KEY, WalletUsername); } if (AppVersion !== undefined) { - this.handleMetadataChanged(APP_VERSION_KEY, AppVersion); + this.handleMetadataUpdated(APP_VERSION_KEY, AppVersion); } if (ChainId !== undefined && JsonRpcUrl !== undefined) { - this.handleChainChanged(ChainId, JsonRpcUrl); + this.handleChainUpdated(ChainId, JsonRpcUrl); } if (EthereumAddress !== undefined) { - this.handleAccountChanged(EthereumAddress); + this.handleAccountUpdated(EthereumAddress); } } private handleDestroyed() { - this.listener?.connectionResetAndReload(); + this.listener?.resetAndReload(); this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { alreadyDestroyed: this.isDestroyed, sessionIdHash: Session.hash(this.sessionId), }); } - private async handleMetadataChanged(key: string, metadataValue: string) { + private async handleMetadataUpdated(key: string, metadataValue: string) { const decryptedValue = await this.decrypt(metadataValue); - this.listener?.connectionMetadataChanged(key, decryptedValue); + this.listener?.metadataUpdated(key, decryptedValue); } - private async handleChainChanged(encryptedChainId: string, encryptedJsonRpcUrl: string) { + private async handleChainUpdated(encryptedChainId: string, encryptedJsonRpcUrl: string) { const chainId = await this.decrypt(encryptedChainId); const jsonRpcUrl = await this.decrypt(encryptedJsonRpcUrl); @@ -493,11 +493,11 @@ export class WalletLinkConnection { this.chainId = chainId; this.jsonRpcUrl = jsonRpcUrl; - this.listener?.connectionChainChanged(chainId, jsonRpcUrl); + this.listener?.chainUpdated(chainId, jsonRpcUrl); } - private async handleAccountChanged(encryptedSelectedAddress: string) { + private async handleAccountUpdated(encryptedSelectedAddress: string) { const selectedAddress = await this.decrypt(encryptedSelectedAddress); - this.listener?.connectionAccountChanged(selectedAddress); + this.listener?.accountUpdated(selectedAddress); } } diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 570a4003dc..444ba40bcd 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -6,7 +6,7 @@ import { EventListener } from '../connection/EventListener'; import { ServerMessageEvent } from '../connection/ServerMessage'; import { WalletLinkConnection, - WalletLinkConnectionListener, + WalletLinkConnectionUpdateListener, } from '../connection/WalletLinkConnection'; import { ErrorType, @@ -138,7 +138,7 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { const connection = new WalletLinkConnection( session, this.linkAPIUrl, - this.connectionListener, + this.listener, this.diagnostic ); @@ -154,13 +154,13 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { return { session, ui, connection }; } - private connectionListener: WalletLinkConnectionListener = { - connectionIncomingEvent: (m: ServerMessageEvent) => { + private listener: WalletLinkConnectionUpdateListener = { + incomingEvent: (m: ServerMessageEvent) => { if (m.event === 'Web3Response') { this.handleIncomingEvent(m); } }, - connectionLinkedUpdated: (linked: boolean) => { + linkedUpdated: (linked: boolean) => { this.isLinked = linked; const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); @@ -183,8 +183,8 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } } }, - connectionMetadataChanged: (key: string, value: string) => this.storage.setItem(key, value), - connectionAccountChanged: (selectedAddress: string) => { + metadataUpdated: (key: string, value: string) => this.storage.setItem(key, value), + accountUpdated: (selectedAddress: string) => { if (this.accountsCallback) { this.accountsCallback([selectedAddress]); } @@ -203,10 +203,9 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { WalletLinkRelay.accountRequestCallbackIds.clear(); } }, - connectionConnectedUpdated: (connected: boolean) => this.ui.setConnected(connected), - connectionChainChanged: (chainId: string, rpcUrl: string) => - this.chainCallback?.(chainId, rpcUrl), - connectionResetAndReload: () => this.resetAndReload(), + connectedUpdated: (connected: boolean) => this.ui.setConnected(connected), + chainUpdated: (chainId: string, rpcUrl: string) => this.chainCallback?.(chainId, rpcUrl), + resetAndReload: () => this.resetAndReload(), }; public attachUI() { From 6e13f106a9f5b163cc7be2e76e8734193fab43f5 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sat, 11 Nov 2023 02:46:58 -0800 Subject: [PATCH 28/50] remove unnecessary exports --- packages/wallet-sdk/src/connection/WalletLinkConnection.ts | 4 ++-- packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index e508fb94a5..b927c19507 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -32,8 +32,8 @@ import { ConnectionState, WalletLinkWebSocket } from './WalletLinkWebSocket'; const HEARTBEAT_INTERVAL = 10000; const REQUEST_TIMEOUT = 60000; -export const WALLET_USER_NAME_KEY = 'walletUsername'; -export const APP_VERSION_KEY = 'AppVersion'; +const WALLET_USER_NAME_KEY = 'walletUsername'; +const APP_VERSION_KEY = 'AppVersion'; export interface WalletLinkConnectionUpdateListener { linkedUpdated: (linked: boolean) => void; diff --git a/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts b/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts index 5e85dc5f32..dbeaeb8961 100644 --- a/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts +++ b/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts @@ -19,9 +19,7 @@ import { Web3Response, } from './Web3Response'; -export const WALLET_USER_NAME_KEY = 'walletUsername'; export const LOCAL_STORAGE_ADDRESSES_KEY = 'Addresses'; -export const APP_VERSION_KEY = 'AppVersion'; export type CancelablePromise = { promise: Promise; From e40c9f51f1b98e99c9efc6d18b7c311481fc2fed Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 14:42:42 -0800 Subject: [PATCH 29/50] cleanup handlers --- .../src/connection/WalletLinkConnection.ts | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index b927c19507..fd0117b97e 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -32,9 +32,6 @@ import { ConnectionState, WalletLinkWebSocket } from './WalletLinkWebSocket'; const HEARTBEAT_INTERVAL = 10000; const REQUEST_TIMEOUT = 60000; -const WALLET_USER_NAME_KEY = 'walletUsername'; -const APP_VERSION_KEY = 'AppVersion'; - export interface WalletLinkConnectionUpdateListener { linkedUpdated: (linked: boolean) => void; connectedUpdated: (connected: boolean) => void; @@ -452,27 +449,27 @@ export class WalletLinkConnection { } private handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { - const { __destroyed, WalletUsername, AppVersion, ChainId, JsonRpcUrl, EthereumAddress } = - metadata; + const handlers = new Map void>([ + ['__destroyed', this.handleDestroyed], + ['EthereumAddress', this.handleAccountUpdated], + ['WalletUsername', this.handleWalletUsernameUpdated], + ['AppVersion', this.handleAppVersionUpdated], + [ + 'ChainId', // ChainId and JsonRpcUrl are always updated together + (v: string) => metadata.JsonRpcUrl && this.handleChainUpdated(v, metadata.JsonRpcUrl), + ], + ]); - if (__destroyed === '1') { - this.handleDestroyed(); - } - if (WalletUsername !== undefined) { - this.handleMetadataUpdated(WALLET_USER_NAME_KEY, WalletUsername); - } - if (AppVersion !== undefined) { - this.handleMetadataUpdated(APP_VERSION_KEY, AppVersion); - } - if (ChainId !== undefined && JsonRpcUrl !== undefined) { - this.handleChainUpdated(ChainId, JsonRpcUrl); - } - if (EthereumAddress !== undefined) { - this.handleAccountUpdated(EthereumAddress); - } + handlers.forEach((handler, key) => { + const value = metadata[key]; + if (value === undefined) return; + handler(value); + }); } - private handleDestroyed() { + private async handleDestroyed(__destroyed: string) { + if (__destroyed !== '1') return; + this.listener?.resetAndReload(); this.diagnostic?.log(EVENTS.METADATA_DESTROYED, { alreadyDestroyed: this.isDestroyed, @@ -480,11 +477,21 @@ export class WalletLinkConnection { }); } - private async handleMetadataUpdated(key: string, metadataValue: string) { - const decryptedValue = await this.decrypt(metadataValue); + private async handleMetadataUpdated(key: string, encryptedMetadataValue: string) { + const decryptedValue = await this.decrypt(encryptedMetadataValue); this.listener?.metadataUpdated(key, decryptedValue); } + private async handleWalletUsernameUpdated(walletUsername: string) { + const WALLET_USER_NAME_KEY = 'walletUsername'; + this.handleMetadataUpdated(WALLET_USER_NAME_KEY, walletUsername); + } + + private async handleAppVersionUpdated(appVersion: string) { + const APP_VERSION_KEY = 'AppVersion'; + this.handleMetadataUpdated(APP_VERSION_KEY, appVersion); + } + private async handleChainUpdated(encryptedChainId: string, encryptedJsonRpcUrl: string) { const chainId = await this.decrypt(encryptedChainId); const jsonRpcUrl = await this.decrypt(encryptedJsonRpcUrl); @@ -496,8 +503,8 @@ export class WalletLinkConnection { this.listener?.chainUpdated(chainId, jsonRpcUrl); } - private async handleAccountUpdated(encryptedSelectedAddress: string) { - const selectedAddress = await this.decrypt(encryptedSelectedAddress); - this.listener?.accountUpdated(selectedAddress); + private async handleAccountUpdated(encryptedEthereumAddress: string) { + const address = await this.decrypt(encryptedEthereumAddress); + this.listener?.accountUpdated(address); } } From b452039f560a05cc35dffc6c701b419e8b469346 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 14:46:31 -0800 Subject: [PATCH 30/50] comment --- .../src/connection/WalletLinkConnection.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index fd0117b97e..f76d3d7c7a 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -449,6 +449,7 @@ export class WalletLinkConnection { } private handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { + // Map of metadata key to handler function const handlers = new Map void>([ ['__destroyed', this.handleDestroyed], ['EthereumAddress', this.handleAccountUpdated], @@ -460,6 +461,7 @@ export class WalletLinkConnection { ], ]); + // call handler for each metadata key if value is defined handlers.forEach((handler, key) => { const value = metadata[key]; if (value === undefined) return; @@ -477,6 +479,11 @@ export class WalletLinkConnection { }); } + private async handleAccountUpdated(encryptedEthereumAddress: string) { + const address = await this.decrypt(encryptedEthereumAddress); + this.listener?.accountUpdated(address); + } + private async handleMetadataUpdated(key: string, encryptedMetadataValue: string) { const decryptedValue = await this.decrypt(encryptedMetadataValue); this.listener?.metadataUpdated(key, decryptedValue); @@ -502,9 +509,4 @@ export class WalletLinkConnection { this.jsonRpcUrl = jsonRpcUrl; this.listener?.chainUpdated(chainId, jsonRpcUrl); } - - private async handleAccountUpdated(encryptedEthereumAddress: string) { - const address = await this.decrypt(encryptedEthereumAddress); - this.listener?.accountUpdated(address); - } } From 8c949f1c02bbdc66262c05e95df20d54ba86574f Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 15:03:04 -0800 Subject: [PATCH 31/50] cleanup --- .../src/connection/WalletLinkConnection.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index f76d3d7c7a..62243cd322 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -48,6 +48,7 @@ export interface WalletLinkConnectionUpdateListener { export class WalletLinkConnection { private ws: WalletLinkWebSocket; private http: WalletLinkHTTP; + private listener?: WalletLinkConnectionUpdateListener; private destroyed = false; private lastHeartbeatResponse = 0; private nextReqId = IntNumber(1); @@ -61,21 +62,18 @@ export class WalletLinkConnection { constructor( session: Session, linkAPIUrl: string, - private listener?: WalletLinkConnectionUpdateListener, + listener: WalletLinkConnectionUpdateListener, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { const sessionId = (this.sessionId = session.id); const sessionKey = (this.sessionKey = session.key); + this.listener = listener; this.decrypt = (cipherText: string) => aes256gcm.decrypt(cipherText, session.secret); const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); - this.ws = ws; - - this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); - - this.ws.setConnectionStateListener(async (state) => { + ws.setConnectionStateListener(async (state) => { // attempt to reconnect every 5 seconds when disconnected this.diagnostic?.log(EVENTS.CONNECTED_STATE_CHANGE, { state, @@ -138,7 +136,6 @@ export class WalletLinkConnection { this.connected = connected; } }); - ws.setIncomingDataListener((m) => { switch (m.type) { // handle server's heartbeat responses @@ -187,6 +184,9 @@ export class WalletLinkConnection { this.requestResolutions.get(m.id)?.(m); } }); + this.ws = ws; + + this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); } /** @@ -448,7 +448,7 @@ export class WalletLinkConnection { this.sendData(msg); } - private handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { + handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { // Map of metadata key to handler function const handlers = new Map void>([ ['__destroyed', this.handleDestroyed], From 2ab0a4e8bd44159faca3335624eecfeef7cd4d17 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 15:04:47 -0800 Subject: [PATCH 32/50] consistency --- .../wallet-sdk/src/connection/WalletLinkConnection.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 091c6c05bb..64b59668e0 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -76,11 +76,7 @@ export class WalletLinkConnection { WebSocketClass: typeof WebSocket = WebSocket ) { const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); - this.ws = ws; - - this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); - - this.ws.setConnectionStateListener(async (state) => { + ws.setConnectionStateListener(async (state) => { // attempt to reconnect every 5 seconds when disconnected this.diagnostic?.log(EVENTS.CONNECTED_STATE_CHANGE, { state, @@ -143,7 +139,6 @@ export class WalletLinkConnection { this.connected = connected; } }); - ws.setIncomingDataListener((m) => { switch (m.type) { // handle server's heartbeat responses @@ -194,6 +189,9 @@ export class WalletLinkConnection { this.requestResolutions.get(m.id)?.(m); } }); + this.ws = ws; + + this.http = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); } /** From 7b48b6f065dca13efd34d87ac32649df83e6966d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 15:19:22 -0800 Subject: [PATCH 33/50] keep distinctUntilChanged on the Relay level --- .../src/connection/WalletLinkConnection.ts | 10 ++-------- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 62243cd322..eac72f4029 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -55,9 +55,6 @@ export class WalletLinkConnection { private readonly sessionId: string; private readonly sessionKey: string; - private readonly decrypt: (_: string) => Promise; - private chainId = ''; - private jsonRpcUrl = ''; constructor( session: Session, @@ -479,6 +476,8 @@ export class WalletLinkConnection { }); } + private readonly decrypt: (_: string) => Promise; + private async handleAccountUpdated(encryptedEthereumAddress: string) { const address = await this.decrypt(encryptedEthereumAddress); this.listener?.accountUpdated(address); @@ -502,11 +501,6 @@ export class WalletLinkConnection { private async handleChainUpdated(encryptedChainId: string, encryptedJsonRpcUrl: string) { const chainId = await this.decrypt(encryptedChainId); const jsonRpcUrl = await this.decrypt(encryptedJsonRpcUrl); - - if (this.chainId === chainId && this.jsonRpcUrl === jsonRpcUrl) return; - - this.chainId = chainId; - this.jsonRpcUrl = jsonRpcUrl; this.listener?.chainUpdated(chainId, jsonRpcUrl); } } diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 444ba40bcd..73cf9d1b14 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -84,6 +84,7 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { protected readonly diagnostic?: DiagnosticLogger; protected connection: WalletLinkConnection; private accountsCallback: ((account: string[], isDisconnect?: boolean) => void) | null = null; + private chainCallbackParams = { chainId: '', jsonRpcUrl: '' }; // to implement distinctUntilChanged private chainCallback: ((chainId: string, jsonRpcUrl: string) => void) | null = null; protected dappDefaultChain = 1; private readonly options: WalletLinkRelayOptions; @@ -184,6 +185,22 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } }, metadataUpdated: (key: string, value: string) => this.storage.setItem(key, value), + chainUpdated: (chainId: string, jsonRpcUrl: string) => { + if ( + this.chainCallbackParams.chainId === chainId && + this.chainCallbackParams.jsonRpcUrl === jsonRpcUrl + ) { + return; + } + this.chainCallbackParams = { + chainId, + jsonRpcUrl, + }; + + if (this.chainCallback) { + this.chainCallback(chainId, jsonRpcUrl); + } + }, accountUpdated: (selectedAddress: string) => { if (this.accountsCallback) { this.accountsCallback([selectedAddress]); @@ -204,7 +221,6 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } }, connectedUpdated: (connected: boolean) => this.ui.setConnected(connected), - chainUpdated: (chainId: string, rpcUrl: string) => this.chainCallback?.(chainId, rpcUrl), resetAndReload: () => this.resetAndReload(), }; From ccc9d328513a7844d38622562e3c36c12d9b83ff Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 15:24:00 -0800 Subject: [PATCH 34/50] keep store key definition on abstract --- packages/wallet-sdk/src/connection/WalletLinkConnection.ts | 3 +-- packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index eac72f4029..c0f1959f26 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -3,6 +3,7 @@ import * as aes256gcm from '../relay/aes256gcm'; import { Session } from '../relay/Session'; +import { APP_VERSION_KEY, WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; import { IntNumber } from '../types'; import { ClientMessage, @@ -489,12 +490,10 @@ export class WalletLinkConnection { } private async handleWalletUsernameUpdated(walletUsername: string) { - const WALLET_USER_NAME_KEY = 'walletUsername'; this.handleMetadataUpdated(WALLET_USER_NAME_KEY, walletUsername); } private async handleAppVersionUpdated(appVersion: string) { - const APP_VERSION_KEY = 'AppVersion'; this.handleMetadataUpdated(APP_VERSION_KEY, appVersion); } diff --git a/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts b/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts index dbeaeb8961..5e85dc5f32 100644 --- a/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts +++ b/packages/wallet-sdk/src/relay/WalletSDKRelayAbstract.ts @@ -19,7 +19,9 @@ import { Web3Response, } from './Web3Response'; +export const WALLET_USER_NAME_KEY = 'walletUsername'; export const LOCAL_STORAGE_ADDRESSES_KEY = 'Addresses'; +export const APP_VERSION_KEY = 'AppVersion'; export type CancelablePromise = { promise: Promise; From 741fa93d77fd9ca4e8cfc7a7d385ed4a7e5ea74a Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 15:52:46 -0800 Subject: [PATCH 35/50] migrate aes256gcm as cipher asdf --- .../src/connection/WalletLinkConnection.ts | 48 ++++++++++++++----- .../WalletLinkConnectionCipher.test.ts} | 2 +- .../WalletLinkConnectionCipher.ts} | 13 +++++ .../src/relay/WalletLinkRelay.test.ts | 29 ----------- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 44 ++--------------- 5 files changed, 52 insertions(+), 84 deletions(-) rename packages/wallet-sdk/src/{relay/aes256gcm.test.ts => connection/WalletLinkConnectionCipher.test.ts} (96%) rename packages/wallet-sdk/src/{relay/aes256gcm.ts => connection/WalletLinkConnectionCipher.ts} (89%) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index c0f1959f26..0296d2960a 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -1,9 +1,10 @@ // Copyright (c) 2018-2023 Coinbase, Inc. // Licensed under the Apache License, version 2.0 -import * as aes256gcm from '../relay/aes256gcm'; +import { RelayMessage } from '../relay/RelayMessage'; import { Session } from '../relay/Session'; import { APP_VERSION_KEY, WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; +import { isWeb3ResponseMessage, Web3ResponseMessage } from '../relay/Web3ResponseMessage'; import { IntNumber } from '../types'; import { ClientMessage, @@ -27,6 +28,7 @@ import { ServerMessageSessionConfigUpdated, } from './ServerMessage'; import { SessionConfig } from './SessionConfig'; +import { WalletLinkConnectionCipher } from './WalletLinkConnectionCipher'; import { WalletLinkHTTP } from './WalletLinkHTTP'; import { ConnectionState, WalletLinkWebSocket } from './WalletLinkWebSocket'; @@ -36,7 +38,7 @@ const REQUEST_TIMEOUT = 60000; export interface WalletLinkConnectionUpdateListener { linkedUpdated: (linked: boolean) => void; connectedUpdated: (connected: boolean) => void; - incomingEvent: (event: ServerMessageEvent) => void; + handleResponseMessage: (message: Web3ResponseMessage) => void; chainUpdated: (chainId: string, jsonRpcUrl: string) => void; accountUpdated: (selectedAddress: string) => void; metadataUpdated: (key: string, metadataValue: string) => void; @@ -49,6 +51,7 @@ export interface WalletLinkConnectionUpdateListener { export class WalletLinkConnection { private ws: WalletLinkWebSocket; private http: WalletLinkHTTP; + private cipher: WalletLinkConnectionCipher; private listener?: WalletLinkConnectionUpdateListener; private destroyed = false; private lastHeartbeatResponse = 0; @@ -66,9 +69,9 @@ export class WalletLinkConnection { ) { const sessionId = (this.sessionId = session.id); const sessionKey = (this.sessionKey = session.key); + this.cipher = new WalletLinkConnectionCipher(session.secret); this.listener = listener; - this.decrypt = (cipherText: string) => aes256gcm.decrypt(cipherText, session.secret); const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); ws.setConnectionStateListener(async (state) => { @@ -281,7 +284,7 @@ export class WalletLinkConnection { }); } - private handleIncomingEvent(m: ServerMessage) { + private async handleIncomingEvent(m: ServerMessage) { function isServerMessageEvent(msg: ServerMessage): msg is ServerMessageEvent { if (msg.type !== 'Event') { return false; @@ -299,7 +302,20 @@ export class WalletLinkConnection { return; } - this.listener?.incomingEvent(m); + if (m.event !== 'Web3Response') { + return; + } + + const decryptedData = await this.cipher.decrypt(m.data); + + const json = JSON.parse(decryptedData); + const message = isWeb3ResponseMessage(json) ? json : null; + + if (!message) { + return; + } + + this.listener?.handleResponseMessage(message); } private shouldFetchUnseenEventsOnConnect = false; @@ -349,11 +365,19 @@ export class WalletLinkConnection { /** * Publish an event and emit event ID when successful * @param event event name - * @param data event data + * @param unencryptedMessage unencrypted event message * @param callWebhook whether the webhook should be invoked * @returns a Promise that emits event ID when successful */ - public async publishEvent(event: string, data: string, callWebhook = false) { + public async publishEvent(event: string, unencryptedMessage: RelayMessage, callWebhook = false) { + const data = await this.cipher.encrypt( + JSON.stringify({ + ...unencryptedMessage, + origin: location.origin, + relaySource: window.coinbaseWalletExtension ? 'injected_sdk' : 'sdk', + }) + ); + const message = ClientMessagePublishEvent({ id: IntNumber(this.nextReqId++), sessionId: this.sessionId, @@ -477,15 +501,13 @@ export class WalletLinkConnection { }); } - private readonly decrypt: (_: string) => Promise; - private async handleAccountUpdated(encryptedEthereumAddress: string) { - const address = await this.decrypt(encryptedEthereumAddress); + const address = await this.cipher.decrypt(encryptedEthereumAddress); this.listener?.accountUpdated(address); } private async handleMetadataUpdated(key: string, encryptedMetadataValue: string) { - const decryptedValue = await this.decrypt(encryptedMetadataValue); + const decryptedValue = await this.cipher.decrypt(encryptedMetadataValue); this.listener?.metadataUpdated(key, decryptedValue); } @@ -498,8 +520,8 @@ export class WalletLinkConnection { } private async handleChainUpdated(encryptedChainId: string, encryptedJsonRpcUrl: string) { - const chainId = await this.decrypt(encryptedChainId); - const jsonRpcUrl = await this.decrypt(encryptedJsonRpcUrl); + const chainId = await this.cipher.decrypt(encryptedChainId); + const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); this.listener?.chainUpdated(chainId, jsonRpcUrl); } } diff --git a/packages/wallet-sdk/src/relay/aes256gcm.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnectionCipher.test.ts similarity index 96% rename from packages/wallet-sdk/src/relay/aes256gcm.test.ts rename to packages/wallet-sdk/src/connection/WalletLinkConnectionCipher.test.ts index a447dd78e0..456ecd07ba 100644 --- a/packages/wallet-sdk/src/relay/aes256gcm.test.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnectionCipher.test.ts @@ -1,5 +1,5 @@ import { randomBytesHex } from '../util'; -import { decrypt, encrypt } from './aes256gcm'; +import { decrypt, encrypt } from './WalletLinkConnectionCipher'; const secret = 'c356fe708ea7bbf7b1cc9ff9813c32772b6e0d16332da4c031ba9ea88be9b5ed'; diff --git a/packages/wallet-sdk/src/relay/aes256gcm.ts b/packages/wallet-sdk/src/connection/WalletLinkConnectionCipher.ts similarity index 89% rename from packages/wallet-sdk/src/relay/aes256gcm.ts rename to packages/wallet-sdk/src/connection/WalletLinkConnectionCipher.ts index 335b8f481b..55c7736103 100644 --- a/packages/wallet-sdk/src/relay/aes256gcm.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnectionCipher.ts @@ -3,6 +3,19 @@ import { hexStringToUint8Array, uint8ArrayToHex } from '../util'; +export class WalletLinkConnectionCipher { + // @param secret hex representation of 32-byte secret + constructor(private readonly secret: string) {} + + async encrypt(plainText: string): Promise { + return encrypt(plainText, this.secret); + } + + async decrypt(cipherText: string): Promise { + return decrypt(cipherText, this.secret); + } +} + /** * * @param plainText string to be encrypted diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts index 0b2790e107..d9da3a96c8 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts @@ -5,7 +5,6 @@ import { SessionConfig } from '../connection/SessionConfig'; import { WalletLinkConnection } from '../connection/WalletLinkConnection'; import { WalletLinkWebSocket } from '../connection/WalletLinkWebSocket'; import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; -import * as aes256gcm from './aes256gcm'; import { WalletLinkRelay, WalletLinkRelayOptions } from './WalletLinkRelay'; import { WalletSDKRelayEventManager } from './WalletSDKRelayEventManager'; @@ -71,35 +70,7 @@ describe('WalletLinkRelay', () => { }); describe('setSessionConfigListener', () => { - it('should update metadata with setSessionConfigListener', async () => { - const sessionConfig: SessionConfig = { - webhookId: 'webhookId', - webhookUrl: 'webhookUrl', - metadata: { - WalletUsername: 'username', - }, - }; - - const decryptSpy = jest.spyOn(aes256gcm, 'decrypt'); - - const relay = new WalletLinkRelay(options); - - (relay as any).connection.ws.incomingDataListener?.({ - ...sessionConfig, - type: 'SessionConfigUpdated', - }); - - expect(decryptSpy).toHaveBeenCalledWith( - sessionConfig.metadata.WalletUsername, - expect.anything() - ); - }); - it('should update chainId and jsonRpcUrl only when distinct', async () => { - jest.spyOn(aes256gcm, 'decrypt').mockImplementation(async (input, _secret) => { - return input; - }); - const callback = jest.fn(); const relay = new WalletLinkRelay(options); relay.setChainCallback(callback); diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 73cf9d1b14..f874732df3 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -3,7 +3,6 @@ import { DiagnosticLogger, EVENTS } from '../connection/DiagnosticLogger'; import { EventListener } from '../connection/EventListener'; -import { ServerMessageEvent } from '../connection/ServerMessage'; import { WalletLinkConnection, WalletLinkConnectionUpdateListener, @@ -20,7 +19,6 @@ import { WalletLinkRelayUI } from '../provider/WalletLinkRelayUI'; import { WalletUI, WalletUIOptions } from '../provider/WalletUI'; import { AddressString, IntNumber, ProviderType, RegExpString } from '../types'; import { bigIntStringFromBN, createQrUrl, hexStringFromBuffer, randomBytesHex } from '../util'; -import * as aes256gcm from './aes256gcm'; import { EthereumTransactionParams } from './EthereumTransactionParams'; import { RelayMessage } from './RelayMessage'; import { Session } from './Session'; @@ -59,7 +57,7 @@ import { WatchAssetResponse, Web3Response, } from './Web3Response'; -import { isWeb3ResponseMessage, Web3ResponseMessage } from './Web3ResponseMessage'; +import { Web3ResponseMessage } from './Web3ResponseMessage'; export interface WalletLinkRelayOptions { linkAPIUrl: string; @@ -100,7 +98,6 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { constructor(options: Readonly) { super(); this.resetAndReload = this.resetAndReload.bind(this); - this.handleIncomingEvent = this.handleIncomingEvent.bind(this); this.linkAPIUrl = options.linkAPIUrl; this.storage = options.storage; @@ -156,11 +153,7 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } private listener: WalletLinkConnectionUpdateListener = { - incomingEvent: (m: ServerMessageEvent) => { - if (m.event === 'Web3Response') { - this.handleIncomingEvent(m); - } - }, + handleResponseMessage: this.handleWeb3ResponseMessage, linkedUpdated: (linked: boolean) => { this.isLinked = linked; const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); @@ -535,38 +528,7 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { message: RelayMessage, callWebhook: boolean ): Promise { - const secret = this.session.secret; - return aes256gcm - .encrypt( - JSON.stringify({ - ...message, - origin: location.origin, - relaySource: window.coinbaseWalletExtension ? 'injected_sdk' : 'sdk', - }), - secret - ) - .then((encrypted: string) => this.connection.publishEvent(event, encrypted, callWebhook)); - } - - private handleIncomingEvent(event: ServerMessageEvent): void { - aes256gcm - .decrypt(event.data, this.session.secret) - .then((decryptedData) => { - const json = JSON.parse(decryptedData); - const message = isWeb3ResponseMessage(json) ? json : null; - - if (!message) { - return; - } - - this.handleWeb3ResponseMessage(message); - }) - .catch(() => { - this.diagnostic?.log(EVENTS.GENERAL_ERROR, { - message: 'Had error decrypting', - value: 'incomingEvent', - }); - }); + return this.connection.publishEvent(event, message, callWebhook); } protected handleWeb3ResponseMessage(message: Web3ResponseMessage) { From 253f8ef1019b9dd6460fef7c97eb2276c399d56b Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 16:30:59 -0800 Subject: [PATCH 36/50] pull out isServerMessageEvent --- .../src/connection/ServerMessage.ts | 11 ++++++ .../src/connection/WalletLinkConnection.ts | 39 ++++++------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/wallet-sdk/src/connection/ServerMessage.ts b/packages/wallet-sdk/src/connection/ServerMessage.ts index 6a2c248e72..86dc376765 100644 --- a/packages/wallet-sdk/src/connection/ServerMessage.ts +++ b/packages/wallet-sdk/src/connection/ServerMessage.ts @@ -76,3 +76,14 @@ export interface ServerMessageEvent extends ServerMessage { event: string; data: string; } + +export function isServerMessageEvent(msg: any): msg is ServerMessageEvent { + return ( + msg && + msg.type === 'Event' && + typeof msg.sessionId === 'string' && + typeof msg.eventId === 'string' && + typeof msg.event === 'string' && + typeof msg.data === 'string' + ); +} diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 0296d2960a..016f8c2120 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -16,9 +16,9 @@ import { } from './ClientMessage'; import { DiagnosticLogger, EVENTS } from './DiagnosticLogger'; import { + isServerMessageEvent, isServerMessageFail, ServerMessage, - ServerMessageEvent, ServerMessageFail, ServerMessageGetSessionConfigOK, ServerMessageIsLinkedOK, @@ -285,37 +285,22 @@ export class WalletLinkConnection { } private async handleIncomingEvent(m: ServerMessage) { - function isServerMessageEvent(msg: ServerMessage): msg is ServerMessageEvent { - if (msg.type !== 'Event') { - return false; - } - const sme = msg as ServerMessageEvent; - return ( - typeof sme.sessionId === 'string' && - typeof sme.eventId === 'string' && - typeof sme.event === 'string' && - typeof sme.data === 'string' - ); - } - - if (!isServerMessageEvent(m)) { - return; - } - - if (m.event !== 'Web3Response') { + if (!isServerMessageEvent(m) || m.event !== 'Web3Response') { return; } - const decryptedData = await this.cipher.decrypt(m.data); - - const json = JSON.parse(decryptedData); - const message = isWeb3ResponseMessage(json) ? json : null; + try { + const decryptedData = await this.cipher.decrypt(m.data); + const message = JSON.parse(decryptedData); - if (!message) { - return; + if (!isWeb3ResponseMessage(message)) return; + this.listener?.handleResponseMessage(message); + } catch { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'incomingEvent', + }); } - - this.listener?.handleResponseMessage(message); } private shouldFetchUnseenEventsOnConnect = false; From fa50dcf3c41aef33ce4da53a2fecbdd46314868d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 16:41:47 -0800 Subject: [PATCH 37/50] keep diagnostics --- .../src/connection/WalletLinkConnection.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 016f8c2120..55558dad39 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -487,13 +487,27 @@ export class WalletLinkConnection { } private async handleAccountUpdated(encryptedEthereumAddress: string) { - const address = await this.cipher.decrypt(encryptedEthereumAddress); - this.listener?.accountUpdated(address); + try { + const address = await this.cipher.decrypt(encryptedEthereumAddress); + this.listener?.accountUpdated(address); + } catch { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'selectedAddress', + }); + } } private async handleMetadataUpdated(key: string, encryptedMetadataValue: string) { - const decryptedValue = await this.cipher.decrypt(encryptedMetadataValue); - this.listener?.metadataUpdated(key, decryptedValue); + try { + const decryptedValue = await this.cipher.decrypt(encryptedMetadataValue); + this.listener?.metadataUpdated(key, decryptedValue); + } catch { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: key, + }); + } } private async handleWalletUsernameUpdated(walletUsername: string) { @@ -505,8 +519,15 @@ export class WalletLinkConnection { } private async handleChainUpdated(encryptedChainId: string, encryptedJsonRpcUrl: string) { - const chainId = await this.cipher.decrypt(encryptedChainId); - const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); - this.listener?.chainUpdated(chainId, jsonRpcUrl); + try { + const chainId = await this.cipher.decrypt(encryptedChainId); + const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); + this.listener?.chainUpdated(chainId, jsonRpcUrl); + } catch { + this.diagnostic?.log(EVENTS.GENERAL_ERROR, { + message: 'Had error decrypting', + value: 'chainId|jsonRpcUrl', + }); + } } } From 674a012bccbd7dff6a499f3eece7dfa7f4570133 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 16:44:06 -0800 Subject: [PATCH 38/50] cleanup --- packages/wallet-sdk/src/relay/WalletLinkRelay.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index f874732df3..789550a2e0 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -153,7 +153,6 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } private listener: WalletLinkConnectionUpdateListener = { - handleResponseMessage: this.handleWeb3ResponseMessage, linkedUpdated: (linked: boolean) => { this.isLinked = linked; const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); @@ -177,7 +176,9 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } } }, - metadataUpdated: (key: string, value: string) => this.storage.setItem(key, value), + metadataUpdated: (key: string, value: string) => { + this.storage.setItem(key, value); // e.g. this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); + }, chainUpdated: (chainId: string, jsonRpcUrl: string) => { if ( this.chainCallbackParams.chainId === chainId && @@ -213,8 +214,11 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { WalletLinkRelay.accountRequestCallbackIds.clear(); } }, - connectedUpdated: (connected: boolean) => this.ui.setConnected(connected), - resetAndReload: () => this.resetAndReload(), + connectedUpdated: (connected: boolean) => { + this.ui.setConnected(connected); + }, + handleResponseMessage: this.handleWeb3ResponseMessage, + resetAndReload: this.resetAndReload, }; public attachUI() { From 7bc3988fbb232c510ef28344243b16920edfbbc5 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 16:47:41 -0800 Subject: [PATCH 39/50] revert setConnected WalletUI changes --- packages/wallet-sdk/src/provider/MobileRelayUI.ts | 2 -- packages/wallet-sdk/src/provider/WalletUI.ts | 2 -- packages/wallet-sdk/src/relay/WalletLinkRelay.ts | 7 +++++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/wallet-sdk/src/provider/MobileRelayUI.ts b/packages/wallet-sdk/src/provider/MobileRelayUI.ts index 0c0d471fbd..148bb6d479 100644 --- a/packages/wallet-sdk/src/provider/MobileRelayUI.ts +++ b/packages/wallet-sdk/src/provider/MobileRelayUI.ts @@ -35,8 +35,6 @@ export class MobileRelayUI implements WalletUI { this.attached = true; } - setConnected(_: boolean): void {} // no-op - closeOpenedWindow() { this.openedWindow?.close(); this.openedWindow = null; diff --git a/packages/wallet-sdk/src/provider/WalletUI.ts b/packages/wallet-sdk/src/provider/WalletUI.ts index f86c51d184..1210ed03fa 100644 --- a/packages/wallet-sdk/src/provider/WalletUI.ts +++ b/packages/wallet-sdk/src/provider/WalletUI.ts @@ -24,8 +24,6 @@ export interface WalletUIOptions { export interface WalletUI { attach(): void; - setConnected(connected: boolean): void; - /** * Opens a qr code or auth page to connect with Coinbase Wallet mobile app * @param options onCancel callback diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 789550a2e0..92dfd6307e 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -177,7 +177,7 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } }, metadataUpdated: (key: string, value: string) => { - this.storage.setItem(key, value); // e.g. this.storage.setItem(WALLET_USER_NAME_KEY, walletUsername); + this.storage.setItem(key, value); }, chainUpdated: (chainId: string, jsonRpcUrl: string) => { if ( @@ -215,7 +215,10 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } }, connectedUpdated: (connected: boolean) => { - this.ui.setConnected(connected); + const { ui } = this; + if (ui instanceof WalletLinkRelayUI) { + ui.setConnected(connected); + } }, handleResponseMessage: this.handleWeb3ResponseMessage, resetAndReload: this.resetAndReload, From 0613cf66923dfa43c816d4d0f8ad1cef0ddbe2dc Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 19:02:30 -0800 Subject: [PATCH 40/50] fix binding issue --- .../src/connection/WalletLinkConnection.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 55558dad39..3769c47c75 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -455,7 +455,7 @@ export class WalletLinkConnection { this.sendData(msg); } - handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { + private handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { // Map of metadata key to handler function const handlers = new Map void>([ ['__destroyed', this.handleDestroyed], @@ -476,7 +476,7 @@ export class WalletLinkConnection { }); } - private async handleDestroyed(__destroyed: string) { + private handleDestroyed = (__destroyed: string) => { if (__destroyed !== '1') return; this.listener?.resetAndReload(); @@ -484,9 +484,9 @@ export class WalletLinkConnection { alreadyDestroyed: this.isDestroyed, sessionIdHash: Session.hash(this.sessionId), }); - } + }; - private async handleAccountUpdated(encryptedEthereumAddress: string) { + private handleAccountUpdated = async (encryptedEthereumAddress: string) => { try { const address = await this.cipher.decrypt(encryptedEthereumAddress); this.listener?.accountUpdated(address); @@ -496,9 +496,9 @@ export class WalletLinkConnection { value: 'selectedAddress', }); } - } + }; - private async handleMetadataUpdated(key: string, encryptedMetadataValue: string) { + private handleMetadataUpdated = async (key: string, encryptedMetadataValue: string) => { try { const decryptedValue = await this.cipher.decrypt(encryptedMetadataValue); this.listener?.metadataUpdated(key, decryptedValue); @@ -508,17 +508,17 @@ export class WalletLinkConnection { value: key, }); } - } + }; - private async handleWalletUsernameUpdated(walletUsername: string) { + private handleWalletUsernameUpdated = async (walletUsername: string) => { this.handleMetadataUpdated(WALLET_USER_NAME_KEY, walletUsername); - } + }; - private async handleAppVersionUpdated(appVersion: string) { + private handleAppVersionUpdated = async (appVersion: string) => { this.handleMetadataUpdated(APP_VERSION_KEY, appVersion); - } + }; - private async handleChainUpdated(encryptedChainId: string, encryptedJsonRpcUrl: string) { + private handleChainUpdated = async (encryptedChainId: string, encryptedJsonRpcUrl: string) => { try { const chainId = await this.cipher.decrypt(encryptedChainId); const jsonRpcUrl = await this.cipher.decrypt(encryptedJsonRpcUrl); @@ -529,5 +529,5 @@ export class WalletLinkConnection { value: 'chainId|jsonRpcUrl', }); } - } + }; } From 69b194847b95227d69dac74da561f3b6527aa904 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 20:08:19 -0800 Subject: [PATCH 41/50] WalletLinkConnection.test --- packages/wallet-sdk/.eslintrc.js | 8 ++ .../connection/WalletLinkConnection.test.ts | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts diff --git a/packages/wallet-sdk/.eslintrc.js b/packages/wallet-sdk/.eslintrc.js index ab64bfe3df..ed8dc11781 100644 --- a/packages/wallet-sdk/.eslintrc.js +++ b/packages/wallet-sdk/.eslintrc.js @@ -8,4 +8,12 @@ module.exports = { rules: { 'no-useless-constructor': 'off', }, + overrides: [ + { + files: ["**/*.test.*"], + rules: { + "@typescript-eslint/no-explicit-any": "off" + } + } + ] }; diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts new file mode 100644 index 0000000000..8f6d4bcd9b --- /dev/null +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts @@ -0,0 +1,78 @@ +import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; +import { Session } from '../relay/Session'; +import { WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; +import { SessionConfig } from './SessionConfig'; +import { WalletLinkConnection, WalletLinkConnectionUpdateListener } from './WalletLinkConnection'; +import { WalletLinkConnectionCipher } from './WalletLinkConnectionCipher'; +import { WalletLinkWebSocket } from './WalletLinkWebSocket'; + +describe('WalletLinkConnection', () => { + const session = new Session(new ScopedLocalStorage('test')); + const linkApiUrl = 'http://link-api-url'; + + let connection: WalletLinkConnection; + let websocket: WalletLinkWebSocket; + let cipher: WalletLinkConnectionCipher; + let listener: WalletLinkConnectionUpdateListener; + + beforeEach(() => { + jest.clearAllMocks(); + connection = new WalletLinkConnection(session, linkApiUrl, { + linkedUpdated: jest.fn(), + connectedUpdated: jest.fn(), + handleResponseMessage: jest.fn(), + chainUpdated: jest.fn(), + accountUpdated: jest.fn(), + metadataUpdated: jest.fn(), + resetAndReload: jest.fn(), + }); + websocket = (connection as any).ws; + cipher = (connection as any).cipher; + listener = (connection as any).listener; + }); + + it('should update metadata with setSessionConfigListener', async () => { + const handleSessionConfigUpdatedSpy = jest.spyOn( + connection as any, + 'handleSessionConfigUpdated' + ); + + const handleWalletUsernameUpdatedSpy = jest.spyOn( + connection as any, + 'handleWalletUsernameUpdated' + ); + + const cipherDecryptSpy = jest + .spyOn(cipher, 'decrypt') + .mockImplementation((text) => Promise.resolve(`decrypted ${text}`)); + + const listenerMetadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); + + // simulate incoming session config + + const sessionConfig: SessionConfig = { + webhookId: 'webhookId', + webhookUrl: 'webhookUrl', + metadata: { + WalletUsername: 'new username', + }, + }; + + (websocket as any).incomingDataListener?.({ + ...sessionConfig, + type: 'SessionConfigUpdated', + }); + + expect(handleSessionConfigUpdatedSpy).toHaveBeenCalledWith(sessionConfig.metadata); + + expect(handleWalletUsernameUpdatedSpy).toHaveBeenCalledWith( + sessionConfig.metadata.WalletUsername + ); + + expect(cipherDecryptSpy).toHaveBeenCalledWith(sessionConfig.metadata.WalletUsername); + + const decrypted = await cipher.decrypt(sessionConfig.metadata.WalletUsername!); + + expect(listenerMetadataUpdatedSpy).toHaveBeenCalledWith(WALLET_USER_NAME_KEY, decrypted); + }); +}); From 04d644493b74b9a428144266b28170f7948f2d91 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 20:47:47 -0800 Subject: [PATCH 42/50] more tests --- .../connection/WalletLinkConnection.test.ts | 107 +++++++++++------- .../src/connection/WalletLinkConnection.ts | 4 +- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts index 8f6d4bcd9b..5ad1cc80d3 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts @@ -1,23 +1,21 @@ import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; import { Session } from '../relay/Session'; -import { WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; +import { APP_VERSION_KEY, WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; import { SessionConfig } from './SessionConfig'; import { WalletLinkConnection, WalletLinkConnectionUpdateListener } from './WalletLinkConnection'; import { WalletLinkConnectionCipher } from './WalletLinkConnectionCipher'; -import { WalletLinkWebSocket } from './WalletLinkWebSocket'; describe('WalletLinkConnection', () => { const session = new Session(new ScopedLocalStorage('test')); - const linkApiUrl = 'http://link-api-url'; let connection: WalletLinkConnection; - let websocket: WalletLinkWebSocket; let cipher: WalletLinkConnectionCipher; let listener: WalletLinkConnectionUpdateListener; beforeEach(() => { jest.clearAllMocks(); - connection = new WalletLinkConnection(session, linkApiUrl, { + + listener = { linkedUpdated: jest.fn(), connectedUpdated: jest.fn(), handleResponseMessage: jest.fn(), @@ -25,54 +23,83 @@ describe('WalletLinkConnection', () => { accountUpdated: jest.fn(), metadataUpdated: jest.fn(), resetAndReload: jest.fn(), - }); - websocket = (connection as any).ws; - cipher = (connection as any).cipher; - listener = (connection as any).listener; + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cipher = { + encrypt: jest.fn().mockImplementation((text) => Promise.resolve(`encrypted ${text}`)), + decrypt: jest.fn().mockImplementation((text) => Promise.resolve(`decrypted ${text}`)), + }; + + connection = new WalletLinkConnection(session, 'http://link-api-url', listener); + (connection as any).cipher = cipher; }); - it('should update metadata with setSessionConfigListener', async () => { - const handleSessionConfigUpdatedSpy = jest.spyOn( - connection as any, - 'handleSessionConfigUpdated' - ); + describe('incomingDataListener', () => { + it('should call handleSessionConfigUpdated when session config is updated', async () => { + const handleSessionConfigUpdatedSpy = jest.spyOn( + connection as any, + 'handleSessionConfigUpdated' + ); - const handleWalletUsernameUpdatedSpy = jest.spyOn( - connection as any, - 'handleWalletUsernameUpdated' - ); + const sessionConfig: SessionConfig = { + webhookId: 'webhookId', + webhookUrl: 'webhookUrl', + metadata: { + WalletUsername: 'new username', + }, + }; - const cipherDecryptSpy = jest - .spyOn(cipher, 'decrypt') - .mockImplementation((text) => Promise.resolve(`decrypted ${text}`)); + (connection as any).ws.incomingDataListener?.({ + ...sessionConfig, + type: 'SessionConfigUpdated', + }); - const listenerMetadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); + expect(handleSessionConfigUpdatedSpy).toHaveBeenCalledWith(sessionConfig.metadata); + }); + }); - // simulate incoming session config + describe('handleSessionConfigUpdated', () => { + let handleSessionConfigUpdated: (metadata: SessionConfig['metadata']) => void; - const sessionConfig: SessionConfig = { - webhookId: 'webhookId', - webhookUrl: 'webhookUrl', - metadata: { - WalletUsername: 'new username', - }, - }; + beforeEach(() => { + handleSessionConfigUpdated = (connection as any).handleSessionConfigUpdated; + }); + + it('should call listner.metadataUpdated when WalletUsername changed', async () => { + const newUsername = 'new username'; + + const listenerMetadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); + + handleSessionConfigUpdated({ WalletUsername: newUsername }); - (websocket as any).incomingDataListener?.({ - ...sessionConfig, - type: 'SessionConfigUpdated', + expect(cipher.decrypt).toHaveBeenCalledWith(newUsername); + expect(listenerMetadataUpdatedSpy).toHaveBeenCalledWith( + WALLET_USER_NAME_KEY, + await cipher.decrypt(newUsername) + ); }); - expect(handleSessionConfigUpdatedSpy).toHaveBeenCalledWith(sessionConfig.metadata); + it('should call listner.metadataUpdated when AppVersion changed', async () => { + const newAppVersion = 'new app version'; - expect(handleWalletUsernameUpdatedSpy).toHaveBeenCalledWith( - sessionConfig.metadata.WalletUsername - ); + const listenerMetadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); - expect(cipherDecryptSpy).toHaveBeenCalledWith(sessionConfig.metadata.WalletUsername); + handleSessionConfigUpdated({ AppVersion: newAppVersion }); - const decrypted = await cipher.decrypt(sessionConfig.metadata.WalletUsername!); + expect(listenerMetadataUpdatedSpy).toHaveBeenCalledWith( + APP_VERSION_KEY, + await cipher.decrypt(newAppVersion) + ); + }); + + it('should call listner.resetAndReload when __destroyed: 1 is received', async () => { + const listenerResetAndReloadSpy = jest.spyOn(listener, 'resetAndReload'); - expect(listenerMetadataUpdatedSpy).toHaveBeenCalledWith(WALLET_USER_NAME_KEY, decrypted); + handleSessionConfigUpdated({ __destroyed: '1' }); + + expect(listenerResetAndReloadSpy).toHaveBeenCalled(); + }); }); }); diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 3769c47c75..3b14eaa016 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -455,7 +455,7 @@ export class WalletLinkConnection { this.sendData(msg); } - private handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { + private handleSessionConfigUpdated = (metadata: SessionConfig['metadata']) => { // Map of metadata key to handler function const handlers = new Map void>([ ['__destroyed', this.handleDestroyed], @@ -474,7 +474,7 @@ export class WalletLinkConnection { if (value === undefined) return; handler(value); }); - } + }; private handleDestroyed = (__destroyed: string) => { if (__destroyed !== '1') return; From 9e8e6eb1d643c0ae6156ae4eb0e8799cc33f230d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 21:05:45 -0800 Subject: [PATCH 43/50] cleaner --- .../connection/WalletLinkConnection.test.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts index 5ad1cc80d3..a0bb49d272 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts @@ -2,36 +2,29 @@ import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; import { Session } from '../relay/Session'; import { APP_VERSION_KEY, WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; import { SessionConfig } from './SessionConfig'; -import { WalletLinkConnection, WalletLinkConnectionUpdateListener } from './WalletLinkConnection'; -import { WalletLinkConnectionCipher } from './WalletLinkConnectionCipher'; +import { WalletLinkConnection } from './WalletLinkConnection'; describe('WalletLinkConnection', () => { const session = new Session(new ScopedLocalStorage('test')); + const listener = { + linkedUpdated: jest.fn(), + connectedUpdated: jest.fn(), + handleResponseMessage: jest.fn(), + chainUpdated: jest.fn(), + accountUpdated: jest.fn(), + metadataUpdated: jest.fn(), + resetAndReload: jest.fn(), + }; + const cipher = { + encrypt: jest.fn().mockImplementation((text) => Promise.resolve(`encrypted ${text}`)), + decrypt: jest.fn().mockImplementation((text) => Promise.resolve(`decrypted ${text}`)), + }; let connection: WalletLinkConnection; - let cipher: WalletLinkConnectionCipher; - let listener: WalletLinkConnectionUpdateListener; beforeEach(() => { jest.clearAllMocks(); - listener = { - linkedUpdated: jest.fn(), - connectedUpdated: jest.fn(), - handleResponseMessage: jest.fn(), - chainUpdated: jest.fn(), - accountUpdated: jest.fn(), - metadataUpdated: jest.fn(), - resetAndReload: jest.fn(), - }; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - cipher = { - encrypt: jest.fn().mockImplementation((text) => Promise.resolve(`encrypted ${text}`)), - decrypt: jest.fn().mockImplementation((text) => Promise.resolve(`decrypted ${text}`)), - }; - connection = new WalletLinkConnection(session, 'http://link-api-url', listener); (connection as any).cipher = cipher; }); @@ -61,11 +54,9 @@ describe('WalletLinkConnection', () => { }); describe('handleSessionConfigUpdated', () => { - let handleSessionConfigUpdated: (metadata: SessionConfig['metadata']) => void; - - beforeEach(() => { - handleSessionConfigUpdated = (connection as any).handleSessionConfigUpdated; - }); + function handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { + (connection as any).handleSessionConfigUpdated(metadata); + } it('should call listner.metadataUpdated when WalletUsername changed', async () => { const newUsername = 'new username'; @@ -101,5 +92,15 @@ describe('WalletLinkConnection', () => { expect(listenerResetAndReloadSpy).toHaveBeenCalled(); }); + + it('should call listner.chainUpdated when ChainId changed', async () => { + const newChainId = 'new chain id'; + + const listenerChainUpdatedSpy = jest.spyOn(listener, 'chainUpdated'); + + handleSessionConfigUpdated({ ChainId: newChainId }); + + expect(listenerChainUpdatedSpy).toHaveBeenCalledWith(await cipher.decrypt(newChainId)); + }); }); }); From 1e6c3274e99908de825d8111d51e4dd728b3bd40 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 21:27:36 -0800 Subject: [PATCH 44/50] tests --- .../connection/WalletLinkConnection.test.ts | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts index a0bb49d272..3ece745ef6 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts @@ -54,53 +54,85 @@ describe('WalletLinkConnection', () => { }); describe('handleSessionConfigUpdated', () => { - function handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { + function invoke_handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { (connection as any).handleSessionConfigUpdated(metadata); } - it('should call listner.metadataUpdated when WalletUsername changed', async () => { - const newUsername = 'new username'; + it('should call listner.metadataUpdated when WalletUsername updated', async () => { + const listener_metadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); - const listenerMetadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); + const newUsername = 'new username'; - handleSessionConfigUpdated({ WalletUsername: newUsername }); + invoke_handleSessionConfigUpdated({ WalletUsername: newUsername }); expect(cipher.decrypt).toHaveBeenCalledWith(newUsername); - expect(listenerMetadataUpdatedSpy).toHaveBeenCalledWith( + expect(listener_metadataUpdatedSpy).toHaveBeenCalledWith( WALLET_USER_NAME_KEY, await cipher.decrypt(newUsername) ); }); - it('should call listner.metadataUpdated when AppVersion changed', async () => { - const newAppVersion = 'new app version'; + it('should call listner.metadataUpdated when AppVersion updated', async () => { + const listener_metadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); - const listenerMetadataUpdatedSpy = jest.spyOn(listener, 'metadataUpdated'); + const newAppVersion = 'new app version'; - handleSessionConfigUpdated({ AppVersion: newAppVersion }); + invoke_handleSessionConfigUpdated({ AppVersion: newAppVersion }); - expect(listenerMetadataUpdatedSpy).toHaveBeenCalledWith( + expect(listener_metadataUpdatedSpy).toHaveBeenCalledWith( APP_VERSION_KEY, await cipher.decrypt(newAppVersion) ); }); it('should call listner.resetAndReload when __destroyed: 1 is received', async () => { - const listenerResetAndReloadSpy = jest.spyOn(listener, 'resetAndReload'); + const listener_resetAndReloadSpy = jest.spyOn(listener, 'resetAndReload'); + + invoke_handleSessionConfigUpdated({ __destroyed: '1' }); + + expect(listener_resetAndReloadSpy).toHaveBeenCalled(); + }); + + it('should call listner.accountUpdated when Account updated', async () => { + const listener_accountUpdatedSpy = jest.spyOn(listener, 'accountUpdated'); - handleSessionConfigUpdated({ __destroyed: '1' }); + const newAccount = 'new account'; - expect(listenerResetAndReloadSpy).toHaveBeenCalled(); + invoke_handleSessionConfigUpdated({ EthereumAddress: newAccount }); + + expect(listener_accountUpdatedSpy).toHaveBeenCalledWith(await cipher.decrypt(newAccount)); }); - it('should call listner.chainUpdated when ChainId changed', async () => { - const newChainId = 'new chain id'; + describe('chain updates', () => { + it('should NOT call listner.chainUpdated when only one changed', async () => { + const listener_chainUpdatedSpy = jest.spyOn(listener, 'chainUpdated'); + + const chainIdUpdate = { ChainId: 'new chain id' }; + const jsonRpcUrlUpdate = { JsonRpcUrl: 'new json rpc url' }; + + invoke_handleSessionConfigUpdated(chainIdUpdate); + invoke_handleSessionConfigUpdated(jsonRpcUrlUpdate); + + await cipher.decrypt(chainIdUpdate.ChainId); - const listenerChainUpdatedSpy = jest.spyOn(listener, 'chainUpdated'); + expect(listener_chainUpdatedSpy).not.toHaveBeenCalled(); + }); + + it('should call listner.chainUpdated when both ChainId and JsonRpcUrl changed', async () => { + const listener_chainUpdatedSpy = jest.spyOn(listener, 'chainUpdated'); - handleSessionConfigUpdated({ ChainId: newChainId }); + const update = { + ChainId: 'new chain id', + JsonRpcUrl: 'new json rpc url', + }; - expect(listenerChainUpdatedSpy).toHaveBeenCalledWith(await cipher.decrypt(newChainId)); + invoke_handleSessionConfigUpdated(update); + + expect(listener_chainUpdatedSpy).toHaveBeenCalledWith( + await cipher.decrypt(update.ChainId), + await cipher.decrypt(update.JsonRpcUrl) + ); + }); }); }); }); From a316bc98fdd0a6b39394c7830ea902afd44112fd Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 21:48:05 -0800 Subject: [PATCH 45/50] reorg --- .../src/connection/WalletLinkConnection.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 3b14eaa016..24b592146e 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -52,14 +52,21 @@ export class WalletLinkConnection { private ws: WalletLinkWebSocket; private http: WalletLinkHTTP; private cipher: WalletLinkConnectionCipher; - private listener?: WalletLinkConnectionUpdateListener; private destroyed = false; private lastHeartbeatResponse = 0; private nextReqId = IntNumber(1); - private readonly sessionId: string; - private readonly sessionKey: string; + private listener?: WalletLinkConnectionUpdateListener; + /** + * Constructor + * @param session Session + * @param linkAPIUrl Coinbase Wallet link server URL + * @param listener WalletLinkConnectionUpdateListener + * @param [WebSocketClass] Custom WebSocket implementation + */ + private sessionId: string; + private sessionKey: string; constructor( session: Session, linkAPIUrl: string, From fd07d5a47d5b5dd6eba85807c5ffe8f0f9cdf30f Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 21:59:16 -0800 Subject: [PATCH 46/50] rename handleSessionConfigUpdated --- .../connection/WalletLinkConnection.test.ts | 28 +++++++++---------- .../src/connection/WalletLinkConnection.ts | 8 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts index 3ece745ef6..f1381a451b 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts @@ -30,10 +30,10 @@ describe('WalletLinkConnection', () => { }); describe('incomingDataListener', () => { - it('should call handleSessionConfigUpdated when session config is updated', async () => { - const handleSessionConfigUpdatedSpy = jest.spyOn( + it('should call handleSessionMetadataUpdated when session config is updated', async () => { + const handleSessionMetadataUpdatedSpy = jest.spyOn( connection as any, - 'handleSessionConfigUpdated' + 'handleSessionMetadataUpdated' ); const sessionConfig: SessionConfig = { @@ -49,13 +49,13 @@ describe('WalletLinkConnection', () => { type: 'SessionConfigUpdated', }); - expect(handleSessionConfigUpdatedSpy).toHaveBeenCalledWith(sessionConfig.metadata); + expect(handleSessionMetadataUpdatedSpy).toHaveBeenCalledWith(sessionConfig.metadata); }); }); - describe('handleSessionConfigUpdated', () => { - function invoke_handleSessionConfigUpdated(metadata: SessionConfig['metadata']) { - (connection as any).handleSessionConfigUpdated(metadata); + describe('handleSessionMetadataUpdated', () => { + function invoke_handleSessionMetadataUpdated(metadata: SessionConfig['metadata']) { + (connection as any).handleSessionMetadataUpdated(metadata); } it('should call listner.metadataUpdated when WalletUsername updated', async () => { @@ -63,7 +63,7 @@ describe('WalletLinkConnection', () => { const newUsername = 'new username'; - invoke_handleSessionConfigUpdated({ WalletUsername: newUsername }); + invoke_handleSessionMetadataUpdated({ WalletUsername: newUsername }); expect(cipher.decrypt).toHaveBeenCalledWith(newUsername); expect(listener_metadataUpdatedSpy).toHaveBeenCalledWith( @@ -77,7 +77,7 @@ describe('WalletLinkConnection', () => { const newAppVersion = 'new app version'; - invoke_handleSessionConfigUpdated({ AppVersion: newAppVersion }); + invoke_handleSessionMetadataUpdated({ AppVersion: newAppVersion }); expect(listener_metadataUpdatedSpy).toHaveBeenCalledWith( APP_VERSION_KEY, @@ -88,7 +88,7 @@ describe('WalletLinkConnection', () => { it('should call listner.resetAndReload when __destroyed: 1 is received', async () => { const listener_resetAndReloadSpy = jest.spyOn(listener, 'resetAndReload'); - invoke_handleSessionConfigUpdated({ __destroyed: '1' }); + invoke_handleSessionMetadataUpdated({ __destroyed: '1' }); expect(listener_resetAndReloadSpy).toHaveBeenCalled(); }); @@ -98,7 +98,7 @@ describe('WalletLinkConnection', () => { const newAccount = 'new account'; - invoke_handleSessionConfigUpdated({ EthereumAddress: newAccount }); + invoke_handleSessionMetadataUpdated({ EthereumAddress: newAccount }); expect(listener_accountUpdatedSpy).toHaveBeenCalledWith(await cipher.decrypt(newAccount)); }); @@ -110,8 +110,8 @@ describe('WalletLinkConnection', () => { const chainIdUpdate = { ChainId: 'new chain id' }; const jsonRpcUrlUpdate = { JsonRpcUrl: 'new json rpc url' }; - invoke_handleSessionConfigUpdated(chainIdUpdate); - invoke_handleSessionConfigUpdated(jsonRpcUrlUpdate); + invoke_handleSessionMetadataUpdated(chainIdUpdate); + invoke_handleSessionMetadataUpdated(jsonRpcUrlUpdate); await cipher.decrypt(chainIdUpdate.ChainId); @@ -126,7 +126,7 @@ describe('WalletLinkConnection', () => { JsonRpcUrl: 'new json rpc url', }; - invoke_handleSessionConfigUpdated(update); + invoke_handleSessionMetadataUpdated(update); expect(listener_chainUpdatedSpy).toHaveBeenCalledWith( await cipher.decrypt(update.ChainId), diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 24b592146e..e42ae7aec1 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -175,9 +175,7 @@ export class WalletLinkConnection { sessionIdHash: Session.hash(sessionId), metadata_keys: msg && msg.metadata ? Object.keys(msg.metadata) : undefined, }); - if (msg.metadata) { - this.handleSessionConfigUpdated(msg.metadata); - } + this.handleSessionMetadataUpdated(msg.metadata); break; } @@ -462,7 +460,9 @@ export class WalletLinkConnection { this.sendData(msg); } - private handleSessionConfigUpdated = (metadata: SessionConfig['metadata']) => { + private handleSessionMetadataUpdated = (metadata: SessionConfig['metadata']) => { + if (!metadata) return; + // Map of metadata key to handler function const handlers = new Map void>([ ['__destroyed', this.handleDestroyed], From 60c8d1a56d1a1a5efb9f39f3f1cb88b6941397d6 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Sun, 12 Nov 2023 22:26:29 -0800 Subject: [PATCH 47/50] tests --- .../connection/WalletLinkConnection.test.ts | 46 +++++++------- .../src/connection/WalletLinkConnection.ts | 1 + .../src/relay/WalletLinkRelay.test.ts | 63 ++++++++++++++++--- .../wallet-sdk/src/relay/WalletLinkRelay.ts | 3 +- 4 files changed, 82 insertions(+), 31 deletions(-) diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts index f1381a451b..48ed5e25c5 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.test.ts @@ -2,31 +2,32 @@ import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; import { Session } from '../relay/Session'; import { APP_VERSION_KEY, WALLET_USER_NAME_KEY } from '../relay/WalletSDKRelayAbstract'; import { SessionConfig } from './SessionConfig'; -import { WalletLinkConnection } from './WalletLinkConnection'; +import { WalletLinkConnection, WalletLinkConnectionUpdateListener } from './WalletLinkConnection'; +import { WalletLinkConnectionCipher } from './WalletLinkConnectionCipher'; + +const decryptMock = jest.fn().mockImplementation((text) => Promise.resolve(`decrypted ${text}`)); + +jest.spyOn(WalletLinkConnectionCipher.prototype, 'decrypt').mockImplementation(decryptMock); describe('WalletLinkConnection', () => { const session = new Session(new ScopedLocalStorage('test')); - const listener = { - linkedUpdated: jest.fn(), - connectedUpdated: jest.fn(), - handleResponseMessage: jest.fn(), - chainUpdated: jest.fn(), - accountUpdated: jest.fn(), - metadataUpdated: jest.fn(), - resetAndReload: jest.fn(), - }; - const cipher = { - encrypt: jest.fn().mockImplementation((text) => Promise.resolve(`encrypted ${text}`)), - decrypt: jest.fn().mockImplementation((text) => Promise.resolve(`decrypted ${text}`)), - }; let connection: WalletLinkConnection; + let listener: WalletLinkConnectionUpdateListener; beforeEach(() => { jest.clearAllMocks(); - connection = new WalletLinkConnection(session, 'http://link-api-url', listener); - (connection as any).cipher = cipher; + connection = new WalletLinkConnection(session, 'http://link-api-url', { + linkedUpdated: jest.fn(), + connectedUpdated: jest.fn(), + handleResponseMessage: jest.fn(), + chainUpdated: jest.fn(), + accountUpdated: jest.fn(), + metadataUpdated: jest.fn(), + resetAndReload: jest.fn(), + }); + listener = (connection as any).listener; }); describe('incomingDataListener', () => { @@ -65,10 +66,9 @@ describe('WalletLinkConnection', () => { invoke_handleSessionMetadataUpdated({ WalletUsername: newUsername }); - expect(cipher.decrypt).toHaveBeenCalledWith(newUsername); expect(listener_metadataUpdatedSpy).toHaveBeenCalledWith( WALLET_USER_NAME_KEY, - await cipher.decrypt(newUsername) + await decryptMock(newUsername) ); }); @@ -81,7 +81,7 @@ describe('WalletLinkConnection', () => { expect(listener_metadataUpdatedSpy).toHaveBeenCalledWith( APP_VERSION_KEY, - await cipher.decrypt(newAppVersion) + await decryptMock(newAppVersion) ); }); @@ -100,7 +100,7 @@ describe('WalletLinkConnection', () => { invoke_handleSessionMetadataUpdated({ EthereumAddress: newAccount }); - expect(listener_accountUpdatedSpy).toHaveBeenCalledWith(await cipher.decrypt(newAccount)); + expect(listener_accountUpdatedSpy).toHaveBeenCalledWith(await decryptMock(newAccount)); }); describe('chain updates', () => { @@ -113,7 +113,7 @@ describe('WalletLinkConnection', () => { invoke_handleSessionMetadataUpdated(chainIdUpdate); invoke_handleSessionMetadataUpdated(jsonRpcUrlUpdate); - await cipher.decrypt(chainIdUpdate.ChainId); + await decryptMock(chainIdUpdate.ChainId); expect(listener_chainUpdatedSpy).not.toHaveBeenCalled(); }); @@ -129,8 +129,8 @@ describe('WalletLinkConnection', () => { invoke_handleSessionMetadataUpdated(update); expect(listener_chainUpdatedSpy).toHaveBeenCalledWith( - await cipher.decrypt(update.ChainId), - await cipher.decrypt(update.JsonRpcUrl) + await decryptMock(update.ChainId), + await decryptMock(update.JsonRpcUrl) ); }); }); diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index e42ae7aec1..cd73c15ef5 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts @@ -299,6 +299,7 @@ export class WalletLinkConnection { const message = JSON.parse(decryptedData); if (!isWeb3ResponseMessage(message)) return; + this.listener?.handleResponseMessage(message); } catch { this.diagnostic?.log(EVENTS.GENERAL_ERROR, { diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts index d9da3a96c8..641f160417 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.test.ts @@ -3,11 +3,22 @@ import { ServerMessageEvent } from '../connection/ServerMessage'; import { SessionConfig } from '../connection/SessionConfig'; import { WalletLinkConnection } from '../connection/WalletLinkConnection'; +import { WalletLinkConnectionCipher } from '../connection/WalletLinkConnectionCipher'; import { WalletLinkWebSocket } from '../connection/WalletLinkWebSocket'; import { ScopedLocalStorage } from '../lib/ScopedLocalStorage'; import { WalletLinkRelay, WalletLinkRelayOptions } from './WalletLinkRelay'; +import { WALLET_USER_NAME_KEY } from './WalletSDKRelayAbstract'; import { WalletSDKRelayEventManager } from './WalletSDKRelayEventManager'; +// mock isWeb3ResponseMessage to return true +jest.mock('./Web3ResponseMessage', () => ({ + isWeb3ResponseMessage: jest.fn().mockReturnValue(true), +})); + +const decryptMock = jest.fn().mockImplementation((text) => Promise.resolve(`"decrypted ${text}"`)); + +jest.spyOn(WalletLinkConnectionCipher.prototype, 'decrypt').mockImplementation(decryptMock); + describe('WalletLinkRelay', () => { const options: WalletLinkRelayOptions = { linkAPIUrl: 'http://link-api-url', @@ -49,11 +60,16 @@ describe('WalletLinkRelay', () => { const relay = new WalletLinkRelay(options); - const handleIncomingEventSpy = jest.spyOn(relay, 'handleIncomingEvent'); + const handleWeb3ResponseMessageSpy = jest.spyOn( + (relay as any).listener, + 'handleResponseMessage' + ); (relay as any).connection.ws.incomingDataListener?.(serverMessageEvent); - expect(handleIncomingEventSpy).toHaveBeenCalledWith(serverMessageEvent); + expect(handleWeb3ResponseMessageSpy).toHaveBeenCalledWith( + JSON.parse(await decryptMock(serverMessageEvent.data)) + ); }); it('should set isLinked with LinkedListener', async () => { @@ -70,6 +86,30 @@ describe('WalletLinkRelay', () => { }); describe('setSessionConfigListener', () => { + it('should update metadata with setSessionConfigListener', async () => { + const sessionConfig: SessionConfig = { + webhookId: 'webhookId', + webhookUrl: 'webhookUrl', + metadata: { + WalletUsername: 'username', + }, + }; + + const relay = new WalletLinkRelay(options); + + const metadataUpdatedSpy = jest.spyOn((relay as any).listener, 'metadataUpdated'); + + (relay as any).connection.ws.incomingDataListener?.({ + ...sessionConfig, + type: 'SessionConfigUpdated', + }); + + expect(metadataUpdatedSpy).toHaveBeenCalledWith( + WALLET_USER_NAME_KEY, + await decryptMock(sessionConfig.metadata.WalletUsername) + ); + }); + it('should update chainId and jsonRpcUrl only when distinct', async () => { const callback = jest.fn(); const relay = new WalletLinkRelay(options); @@ -89,8 +129,10 @@ describe('WalletLinkRelay', () => { ...sessionConfig, type: 'GetSessionConfigOK', }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(callback).toHaveBeenCalledWith('ChainId', 'JsonRpcUrl'); + expect(callback).toHaveBeenCalledWith( + await decryptMock(sessionConfig.metadata.ChainId), + await decryptMock(sessionConfig.metadata.JsonRpcUrl) + ); // same chain id and json rpc url (relay as any).connection.ws.incomingDataListener?.({ @@ -100,16 +142,23 @@ describe('WalletLinkRelay', () => { expect(callback).toHaveBeenCalledTimes(1); // distinctUntilChanged // different chain id and json rpc url - (relay as any).connection.ws.incomingDataListener?.({ + const newSessionConfig = { ...sessionConfig, metadata: { ChainId: 'ChainId2', JsonRpcUrl: 'JsonRpcUrl2', }, + }; + + (relay as any).connection.ws.incomingDataListener?.({ + ...newSessionConfig, type: 'SessionConfigUpdated', }); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(callback).toHaveBeenCalledWith('ChainId2', 'JsonRpcUrl2'); + + expect(callback).toHaveBeenCalledWith( + await decryptMock(newSessionConfig.metadata.ChainId), + await decryptMock(newSessionConfig.metadata.JsonRpcUrl) + ); expect(callback).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts index 92dfd6307e..56a5609144 100644 --- a/packages/wallet-sdk/src/relay/WalletLinkRelay.ts +++ b/packages/wallet-sdk/src/relay/WalletLinkRelay.ts @@ -153,6 +153,8 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { } private listener: WalletLinkConnectionUpdateListener = { + handleResponseMessage: this.handleWeb3ResponseMessage, + linkedUpdated: (linked: boolean) => { this.isLinked = linked; const cachedAddresses = this.storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); @@ -220,7 +222,6 @@ export class WalletLinkRelay extends WalletSDKRelayAbstract { ui.setConnected(connected); } }, - handleResponseMessage: this.handleWeb3ResponseMessage, resetAndReload: this.resetAndReload, }; From 28f60fc47a163b540a26f753e649e8a19e5ba9d2 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Mon, 13 Nov 2023 00:28:11 -0800 Subject: [PATCH 48/50] tests --- .../src/connection/WalletLinkHTTP.test.ts | 115 ++++++++++++++++++ .../src/connection/WalletLinkHTTP.ts | 4 +- 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts new file mode 100644 index 0000000000..f755e19a1b --- /dev/null +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts @@ -0,0 +1,115 @@ +import { ServerMessageEvent } from './ServerMessage'; +import { WalletLinkHTTP } from './WalletLinkHTTP'; + +describe('WalletLinkHTTP', () => { + const linkAPIUrl = 'https://example.com'; + const sessionId = '123'; + const sessionKey = 'abc'; + + it('should construct a WalletLinkHTTP instance with auth header', () => { + const walletLinkHTTP = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); + + expect((walletLinkHTTP as any).auth).toEqual('Basic MTIzOmFiYw=='); + }); + + describe('fetchUnseenEvents', () => { + let events: { + id: string; + event: 'Web3Request' | 'Web3Response' | 'Web3RequestCanceled'; + data: string; + }[]; + + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => ({ + events, + timestamp: 123, + }), + }); + + beforeEach(() => { + events = []; + }); + + describe('fetchUnseenEvents', () => { + it('should return an empty array if there are no unseen events', async () => { + const walletLinkHTTP = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); + jest.spyOn(walletLinkHTTP as any, 'markUnseenEventsAsSeen').mockImplementation(() => {}); + + const result = await walletLinkHTTP.fetchUnseenEvents(); + + expect(result).toEqual([]); + }); + + it('should return an array of unseen events', async () => { + events = [ + { + id: '1', + event: 'Web3Response', + data: 'data 1', + }, + { + id: '2', + event: 'Web3Response', + data: 'data 2', + }, + ]; + + const walletLinkHTTP = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); + jest.spyOn(walletLinkHTTP as any, 'markUnseenEventsAsSeen').mockImplementation(() => {}); + + const result = await walletLinkHTTP.fetchUnseenEvents(); + + expect(result).toEqual([ + { + type: 'Event', + sessionId: '123', + eventId: '1', + event: 'Web3Response', + data: 'data 1', + }, + { + type: 'Event', + sessionId: '123', + eventId: '2', + event: 'Web3Response', + data: 'data 2', + }, + ]); + }); + }); + + describe('markUnseenEventsAsSeen', () => { + it('should mark all unseen events as seen', () => { + const walletLinkHTTP = new WalletLinkHTTP(linkAPIUrl, sessionId, sessionKey); + const unseenEvents: ServerMessageEvent[] = [ + { + type: 'Event', + sessionId: '1', + eventId: 'id-1', + event: 'Web3Response', + data: 'data 1', + }, + { + type: 'Event', + sessionId: '2', + eventId: 'id-2', + event: 'Web3Response', + data: 'data 2', + }, + ]; + + // spy on fetch and verify that it was called with the correct arguments + const fetchSpy = jest.spyOn(global, 'fetch'); + + (walletLinkHTTP as any).markUnseenEventsAsSeen(unseenEvents); + + const metadata = expect.objectContaining({ headers: expect.anything(), method: 'POST' }); + + expect(fetchSpy).toHaveBeenCalledWith('https://example.com/events/id-1/seen', metadata); + expect(fetchSpy).toHaveBeenCalledWith('https://example.com/events/id-2/seen', metadata); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + }); + }); +}); diff --git a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts index f1aecf3c52..fd5bf3d5be 100644 --- a/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -13,8 +13,8 @@ export class WalletLinkHTTP { } // mark unseen events as seen - private markUnseenEventsAsSeen(events: ServerMessageEvent[]) { - Promise.all( + private async markUnseenEventsAsSeen(events: ServerMessageEvent[]) { + return Promise.all( events.map((e) => fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { method: 'POST', From 322b1d2611facc9498a2f8c5fe9c771b4ff6da2d Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Mon, 13 Nov 2023 00:32:43 -0800 Subject: [PATCH 49/50] update jest config --- packages/wallet-sdk/jest.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wallet-sdk/jest.config.ts b/packages/wallet-sdk/jest.config.ts index c6eaee20e4..b2d8e62d0e 100644 --- a/packages/wallet-sdk/jest.config.ts +++ b/packages/wallet-sdk/jest.config.ts @@ -16,7 +16,8 @@ export default { './src/errors.ts', './src/CoinbaseWalletSDK.ts', './src/connection/RxWebSocket.ts', - './src/connection/WalletSDKConnection.ts', + './src/connection/WalletLinkConnection.ts', + './src/connection/WalletLinkHTTP.ts', './src/lib/ScopedLocalStorage.ts', './src/provider/CoinbaseWalletProvider.ts', './src/provider/FilterPolyfill.ts', From 8f89b2411aca5204bf527fa7f62c69499195bad7 Mon Sep 17 00:00:00 2001 From: Jungho Bang Date: Mon, 13 Nov 2023 00:35:28 -0800 Subject: [PATCH 50/50] update jest config --- packages/wallet-sdk/jest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet-sdk/jest.config.ts b/packages/wallet-sdk/jest.config.ts index b2d8e62d0e..54ef9b71cb 100644 --- a/packages/wallet-sdk/jest.config.ts +++ b/packages/wallet-sdk/jest.config.ts @@ -18,12 +18,12 @@ export default { './src/connection/RxWebSocket.ts', './src/connection/WalletLinkConnection.ts', './src/connection/WalletLinkHTTP.ts', + './src/connection/WalletLinkConnectionCipher.ts', './src/lib/ScopedLocalStorage.ts', './src/provider/CoinbaseWalletProvider.ts', './src/provider/FilterPolyfill.ts', './src/provider/SubscriptionManager.ts', './src/provider/WalletLinkRelayUI.ts', - './src/relay/aes256gcm.ts', './src/relay/Session.ts', './src/relay/WalletSDKRelay.ts', './src/relay/WalletSDKRelayEventManager.ts',