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', diff --git a/packages/wallet-sdk/src/connection/WalletLinkConnection.ts b/packages/wallet-sdk/src/connection/WalletLinkConnection.ts index 4c92723cb0..64b59668e0 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); @@ -69,14 +71,12 @@ export class WalletLinkConnection { constructor( private sessionId: string, private sessionKey: string, - private linkAPIUrl: string, + linkAPIUrl: string, private diagnostic?: DiagnosticLogger, WebSocketClass: typeof WebSocket = WebSocket ) { const ws = new WalletLinkWebSocket(`${linkAPIUrl}/rpc`, WebSocketClass); - this.ws = ws; - - 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, @@ -139,7 +139,6 @@ export class WalletLinkConnection { this.connected = connected; } }); - ws.setIncomingDataListener((m) => { switch (m.type) { // handle server's heartbeat responses @@ -190,23 +189,9 @@ export class WalletLinkConnection { this.requestResolutions.get(m.id)?.(m); } }); - } + this.ws = ws; - // 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); } /** @@ -346,46 +331,8 @@ 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}`); - } + const responseEvents = await this.http.fetchUnseenEvents(); + responseEvents.forEach((e) => this.handleIncomingEvent(e)); } /** 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 new file mode 100644 index 0000000000..fd5bf3d5be --- /dev/null +++ b/packages/wallet-sdk/src/connection/WalletLinkHTTP.ts @@ -0,0 +1,68 @@ +import { ServerMessageEvent } from './ServerMessage'; + +export class WalletLinkHTTP { + private readonly auth: string; + + constructor( + private readonly linkAPIUrl: string, + private readonly sessionId: string, + sessionKey: string + ) { + const credentials = `${sessionId}:${sessionKey}`; + this.auth = `Basic ${btoa(credentials)}`; + } + + // mark unseen events as seen + private async markUnseenEventsAsSeen(events: ServerMessageEvent[]) { + return Promise.all( + events.map((e) => + fetch(`${this.linkAPIUrl}/events/${e.eventId}/seen`, { + method: 'POST', + headers: { + Authorization: this.auth, + }, + }) + ) + ).catch((error) => console.error('Unabled to mark event as failed:', error)); + } + + async fetchUnseenEvents(): Promise { + const response = await fetch(`${this.linkAPIUrl}/events?unseen=true`, { + headers: { + Authorization: this.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, + })) ?? []; + + this.markUnseenEventsAsSeen(responseEvents); + + return responseEvents; + } + throw new Error(`Check unseen events failed: ${response.status}`); + } +}