Skip to content

Commit

Permalink
Separate out HTTP API calls as a dedicated file (#1058)
Browse files Browse the repository at this point in the history
* asdf

* asdf

* asdf

* separate out http stuff

* swap

* linkAPIUrl no need after init

* cleanup

* for each

* constructor

* consistency

* tests

* update jest config
  • Loading branch information
bangtoven authored Nov 13, 2023
1 parent 9a5ebab commit 0381d05
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 62 deletions.
3 changes: 2 additions & 1 deletion packages/wallet-sdk/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
69 changes: 8 additions & 61 deletions packages/wallet-sdk/src/connection/WalletLinkConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -139,7 +139,6 @@ export class WalletLinkConnection {
this.connected = connected;
}
});

ws.setIncomingDataListener((m) => {
switch (m.type) {
// handle server's heartbeat responses
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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));
}

/**
Expand Down
115 changes: 115 additions & 0 deletions packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts
Original file line number Diff line number Diff line change
@@ -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==');

Check warning on line 12 in packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts

View workflow job for this annotation

GitHub Actions / Lint Check

Unexpected any. Specify a different type
});

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(() => {});

Check warning on line 37 in packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts

View workflow job for this annotation

GitHub Actions / Lint Check

Unexpected any. Specify a different type

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(() => {});

Check warning on line 59 in packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts

View workflow job for this annotation

GitHub Actions / Lint Check

Unexpected any. Specify a different type

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);

Check warning on line 105 in packages/wallet-sdk/src/connection/WalletLinkHTTP.test.ts

View workflow job for this annotation

GitHub Actions / Lint Check

Unexpected any. Specify a different type

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);
});
});
});
});
68 changes: 68 additions & 0 deletions packages/wallet-sdk/src/connection/WalletLinkHTTP.ts
Original file line number Diff line number Diff line change
@@ -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<ServerMessageEvent[]> {
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}`);
}
}

0 comments on commit 0381d05

Please sign in to comment.