Skip to content

Commit 556b51a

Browse files
Small improvements (#440)
- Clears associated recovery callbacks when closing a connection (see [this convo](#431 (comment))) - Simplify some code by embedding the network field on the `WebSocketConnection` entity - Can now manually close connections from the test dapp: ![image](https://github.com/user-attachments/assets/9c90d100-03a3-49cf-bdfb-127be4bc3e77)
1 parent 3de1292 commit 556b51a

11 files changed

Lines changed: 242 additions & 87 deletions

File tree

packages/site/src/components/Handlers/WebSockets.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ export const WebSockets = () => {
2323
});
2424
};
2525

26+
const closeAllConnections = async () => {
27+
showToasterForResponse(
28+
{ result: 'ok' },
29+
{
30+
title: 'Closed all WebSocket connections',
31+
},
32+
);
33+
await invokeSnap({
34+
method: TestDappRpcRequestMethod.TestCloseAllConnections,
35+
});
36+
};
37+
2638
const subscribeToAccount = async () => {
2739
await invokeSnap({
2840
method: TestDappRpcRequestMethod.TestSubscribeToAccount,
@@ -52,6 +64,13 @@ export const WebSockets = () => {
5264
>
5365
Setup All Connections
5466
</Button>
67+
<Button
68+
variant="outline"
69+
onClick={closeAllConnections}
70+
marginRight="1"
71+
>
72+
Close All Connections
73+
</Button>
5574
</Flex>
5675
<Flex>
5776
<Button

packages/snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-solana-wallet.git"
88
},
99
"source": {
10-
"shasum": "g/jfpUCvmwu6piUwA21tuyxrmaZZnZl3JdLeVJysYFM=",
10+
"shasum": "EBrVuE+uXKdUKPSGja1momxWXqItfVGppjHtfMaDo+I=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/snap/src/core/handlers/onRpcRequest/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ export const handlers: Record<RpcRequestMethod, OnRpcRequestHandler> = {
1818
await eventEmitter.emitSync('onTestSubscribeToAccount');
1919
return null;
2020
},
21+
[TestDappRpcRequestMethod.TestCloseAllConnections as any]: async () => {
22+
await eventEmitter.emitSync('onTestCloseAllConnections');
23+
return null;
24+
},
2125
};

packages/snap/src/core/handlers/onRpcRequest/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export enum RpcRequestMethod {
1010
export enum TestDappRpcRequestMethod {
1111
TestSetupAllConnections = 'testSetupAllConnections',
1212
TestSubscribeToAccount = 'testSubscribeToAccount',
13+
TestCloseAllConnections = 'testCloseAllConnections',
1314
}

packages/snap/src/core/services/subscriptions/WebSocketConnectionRepository.test.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1+
import { Network } from '../../constants/solana';
2+
import type { ConfigProvider } from '../config';
13
import { WebSocketConnectionRepository } from './WebSocketConnectionRepository';
24

5+
const mockNetwork = Network.Mainnet;
6+
const mockWebSocketUrl = 'ws://localhost:8080';
7+
const mockConnectionId = 'some-connection-id';
8+
39
describe('WebSocketConnectionRepository', () => {
410
let repository: WebSocketConnectionRepository;
11+
let mockConfigProvider: ConfigProvider;
512

613
beforeEach(() => {
7-
repository = new WebSocketConnectionRepository();
14+
mockConfigProvider = {
15+
getNetworkBy: jest.fn().mockReturnValue({
16+
caip2Id: mockNetwork,
17+
webSocketUrl: mockWebSocketUrl,
18+
}),
19+
} as unknown as ConfigProvider;
20+
21+
repository = new WebSocketConnectionRepository(mockConfigProvider);
822

923
const snap = {
1024
request: jest.fn(),
@@ -24,11 +38,11 @@ describe('WebSocketConnectionRepository', () => {
2438
it('returns all connections opened in the extension', async () => {
2539
jest
2640
.spyOn(snap, 'request')
27-
.mockResolvedValue([{ id: '1', url: 'ws://localhost:8080' }]);
41+
.mockResolvedValue([{ id: mockConnectionId, url: mockWebSocketUrl }]);
2842

2943
const connections = await repository.getAll();
3044
expect(connections).toStrictEqual([
31-
{ id: '1', url: 'ws://localhost:8080' },
45+
{ id: mockConnectionId, url: mockWebSocketUrl, network: mockNetwork },
3246
]);
3347
});
3448
});
@@ -45,53 +59,70 @@ describe('WebSocketConnectionRepository', () => {
4559
it('returns the connection when it exists', async () => {
4660
jest
4761
.spyOn(snap, 'request')
48-
.mockResolvedValue([{ id: '1', url: 'ws://localhost:8080' }]);
62+
.mockResolvedValue([{ id: mockConnectionId, url: mockWebSocketUrl }]);
4963

50-
const connection = await repository.getById('1');
64+
const connection = await repository.getById(mockConnectionId);
5165

52-
expect(connection).toStrictEqual({ id: '1', url: 'ws://localhost:8080' });
66+
expect(connection).toStrictEqual({
67+
id: mockConnectionId,
68+
url: mockWebSocketUrl,
69+
network: mockNetwork,
70+
});
5371
});
5472
});
5573

5674
describe('findByUrl', () => {
5775
it('returns null when the connection does not exist', async () => {
5876
jest.spyOn(snap, 'request').mockResolvedValue([]);
5977

60-
const connection = await repository.findByUrl('ws://localhost:8080');
78+
const connection = await repository.findByNetwork(mockNetwork);
6179

6280
expect(connection).toBeNull();
6381
});
6482

6583
it('returns the connection when it exists', async () => {
6684
jest
6785
.spyOn(snap, 'request')
68-
.mockResolvedValue([{ id: '1', url: 'ws://localhost:8080' }]);
86+
.mockResolvedValue([{ id: mockConnectionId, url: mockWebSocketUrl }]);
6987

70-
const connection = await repository.findByUrl('ws://localhost:8080');
88+
const connection = await repository.findByNetwork(mockNetwork);
7189

72-
expect(connection).toStrictEqual({ id: '1', url: 'ws://localhost:8080' });
90+
expect(connection).toStrictEqual({
91+
id: mockConnectionId,
92+
url: mockWebSocketUrl,
93+
network: mockNetwork,
94+
});
7395
});
7496
});
7597

7698
describe('save', () => {
7799
it('saves the connection', async () => {
78-
jest.spyOn(snap, 'request').mockResolvedValue('1');
100+
jest.spyOn(snap, 'request').mockResolvedValue(mockConnectionId);
79101

80-
const connectionId = await repository.save('ws://localhost:8080');
102+
const connection = await repository.save({
103+
network: mockNetwork,
104+
url: mockWebSocketUrl,
105+
protocols: [],
106+
});
81107

82-
expect(connectionId).toBe('1');
108+
expect(connection).toStrictEqual({
109+
id: mockConnectionId,
110+
network: mockNetwork,
111+
url: mockWebSocketUrl,
112+
protocols: [],
113+
});
83114
});
84115
});
85116

86117
describe('delete', () => {
87118
it('deletes the connection', async () => {
88119
jest.spyOn(snap, 'request').mockResolvedValue(null);
89120

90-
await repository.delete('1');
121+
await repository.delete(mockConnectionId);
91122

92123
expect(snap.request).toHaveBeenCalledWith({
93124
method: 'snap_closeWebSocket',
94-
params: { id: '1' },
125+
params: { id: mockConnectionId },
95126
});
96127
});
97128
});
Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { WebSocketConnection } from '../../../entities';
2+
import type { Network } from '../../constants/solana';
3+
import type { ConfigProvider } from '../config';
24

35
/**
46
* Repository that is treating the Snap's WebSocket storage as a persistent data store, where:
@@ -9,43 +11,76 @@ import type { WebSocketConnection } from '../../../entities';
911
* It also tracks bidirectional mappings between the connection ID and the URL to perform fast lookups.
1012
*/
1113
export class WebSocketConnectionRepository {
14+
readonly #configProvider: ConfigProvider;
15+
16+
constructor(configProvider: ConfigProvider) {
17+
this.#configProvider = configProvider;
18+
}
19+
20+
/**
21+
* Gets all connections.
22+
* @returns All connections.
23+
*/
1224
async getAll(): Promise<WebSocketConnection[]> {
13-
return snap.request({
25+
const snapConnections = await snap.request({
1426
method: 'snap_getWebSockets',
1527
});
28+
29+
// Enhance the connections with the network
30+
return snapConnections.map((connection) => ({
31+
...connection,
32+
network: this.#findNetworkByWebSocketUrl(connection.url),
33+
}));
1634
}
1735

36+
/**
37+
* Gets the connection for the specified ID.
38+
* @param id - The ID of the connection to get.
39+
* @returns The connection, or null if no connection exists for the ID.
40+
*/
1841
async getById(id: string): Promise<WebSocketConnection | null> {
1942
const existingConnections = await this.getAll();
2043
return (
2144
existingConnections.find((connection) => connection.id === id) ?? null
2245
);
2346
}
2447

25-
async findByUrl(url: string): Promise<WebSocketConnection | null> {
48+
/**
49+
* Finds the connection for the specified network.
50+
* @param network - The network to find the connection for.
51+
* @returns The connection, or null if no connection exists for the network.
52+
*/
53+
async findByNetwork(network: Network): Promise<WebSocketConnection | null> {
2654
const existingConnections = await this.getAll();
27-
2855
return (
29-
existingConnections.find((connection) => connection.url === url) ?? null
56+
existingConnections.find(
57+
(connection) => connection.network === network,
58+
) ?? null
3059
);
3160
}
3261

3362
/**
3463
* Creates a new connection to the specified URL.
35-
* @param url - The URL of the connection.
36-
* @param protocols - The protocols of the connection.
64+
* @param connection - The connection to create, without the `id` field.
3765
* @returns The connection ID.
3866
*/
39-
async save(url: string, protocols?: string[]) {
40-
const connectionId = await snap.request({
67+
async save(
68+
connection: Omit<WebSocketConnection, 'id'>,
69+
): Promise<WebSocketConnection> {
70+
const { url, protocols } = connection;
71+
72+
const id = await snap.request({
4173
method: 'snap_openWebSocket',
4274
params: {
4375
url,
4476
...(protocols ? { protocols } : {}),
4577
},
4678
});
4779

48-
return connectionId;
80+
return {
81+
...connection,
82+
id,
83+
};
4984
}
5085

5186
/**
@@ -58,4 +93,22 @@ export class WebSocketConnectionRepository {
5893
params: { id },
5994
});
6095
}
96+
97+
/**
98+
* Gets the network for the specified connection ID.
99+
* @param webSocketUrl - The WebSocket URL to get the network for.
100+
* @returns The network.
101+
*/
102+
#findNetworkByWebSocketUrl(webSocketUrl: string): Network {
103+
const network = this.#configProvider.getNetworkBy(
104+
'webSocketUrl',
105+
webSocketUrl,
106+
);
107+
108+
if (!network) {
109+
throw new Error(`No network found for WebSocket URL: ${webSocketUrl}`);
110+
}
111+
112+
return network.caip2Id;
113+
}
61114
}

0 commit comments

Comments
 (0)