Skip to content

Commit 5c68f0b

Browse files
Nwnt 381 solana snap initial websockets setup (#431)
## WebSocket Connection Management Introduces an autonomous `ConnectionManagerPort` interface and related implementation `ConnectionManagerAdapter`, in charge of: - keeping WebSocket connections open (`onStart`, `onInstall`, `onUpdate`) for the active networks using SIP-20's [`snap_openWebSocket`](https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-20.md#snap_openwebsocket). - closing WebSocket connections for inactive networks using SIP-20's [`snap_openWebSocket`](https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-20.md#snap_openwebsocket). - re-opening WebSocket connections on disconnect event - relies on a `ConnectionRepository` that handles opening and closing in a CRUD style ## RPC Subscription Management Introduces a `SubscriberPort` interface and related implementation `SubscriberAdapter`, that lets consumers: - subscribe to RPC WebSocket methods - unsubscribe from RPC WebSocket methods - re-subscribe in case of disconnect event - relies on a `SubscriptionRepository` that handles CRUD operations on subscriptions, persisted in state ## Event Emitter Introduction: Improved Architecture & Encapsulation This PR introduces a centralized `EventEmitter` class that provides a clean, decoupled way to handle snap events `onStart`, `onInstall`, `onUpdate`, and SIP-20 [`onWebSocketEvent`](https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-20.md#handling-websocket-events). The event emitter replaces direct method calls and avoids tight coupling between components with a publish-subscribe pattern. Benefits of encapsulating logic: - Before: Components directly called each other's methods, creating tight coupling and making the codebase harder to test and maintain. - After: Components emit and listen to events, allowing for: - Separation of Concerns: Each component focuses on its core responsibility - Loose Coupling: Components don't need to know about each other's implementation details - Better Testability: Events can be easily mocked and tested in isolation - Extensibility: New listeners can be added without modifying existing code ## Testing - Adds a section in the Test Dapp with buttons to manually test WebSocket connections and subscription - Introduces a new set of `onRpcRequest` handlers to handle clicks on these buttons. ![image](https://github.com/user-attachments/assets/63555d2d-b8bf-4db3-a6dd-5880d37c95c1) ## Missing - In the case where the snap restarts (not the extension, just the snap), the callbacks associated with previously created subscriptions are lost (subscriptions are persisted in state, but callbacks are non JSON-serializable. We need to come up with a solution for this. - Even though callbacks are bound to `onInstall`, `onUpdate`, `onStart`, they don't seem to get called.
1 parent 0522240 commit 5c68f0b

52 files changed

Lines changed: 2939 additions & 24 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-lint-test.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ on:
1111
required: true
1212
RPC_URL_LOCALNET_LIST:
1313
required: true
14+
RPC_WEB_SOCKET_URL_MAINNET:
15+
required: true
16+
RPC_WEB_SOCKET_URL_DEVNET:
17+
required: true
18+
RPC_WEB_SOCKET_URL_TESTNET:
19+
required: true
20+
RPC_WEB_SOCKET_URL_LOCALNET:
21+
required: true
1422
PRICE_API_BASE_URL:
1523
required: true
1624
LOCAL_API_BASE_URL:
@@ -47,6 +55,10 @@ jobs:
4755
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
4856
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
4957
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
58+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
59+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
60+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
61+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
5062
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
5163
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
5264
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
@@ -94,6 +106,10 @@ jobs:
94106
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
95107
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
96108
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
109+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
110+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
111+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
112+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
97113
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
98114
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
99115
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}

.github/workflows/create-release-pr.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ jobs:
5959
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
6060
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
6161
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
62+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
63+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
64+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
65+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
6266
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
6367
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
6468
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}

.github/workflows/main.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ jobs:
1313
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
1414
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
1515
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
16+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
17+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
18+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
19+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
1620
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
1721
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
1822
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}
@@ -87,6 +91,10 @@ jobs:
8791
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
8892
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
8993
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
94+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
95+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
96+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
97+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
9098
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
9199
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
92100
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}

.github/workflows/pr.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ jobs:
2727
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
2828
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
2929
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
30+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
31+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
32+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
33+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
3034
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
3135
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
3236
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}

.github/workflows/publish-release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ on:
1515
required: true
1616
RPC_URL_LOCALNET_LIST:
1717
required: true
18+
RPC_WEB_SOCKET_URL_MAINNET:
19+
required: true
20+
RPC_WEB_SOCKET_URL_DEVNET:
21+
required: true
22+
RPC_WEB_SOCKET_URL_TESTNET:
23+
required: true
24+
RPC_WEB_SOCKET_URL_LOCALNET:
25+
required: true
1826
PRICE_API_BASE_URL:
1927
required: true
2028
LOCAL_API_BASE_URL:
@@ -51,6 +59,10 @@ jobs:
5159
RPC_URL_DEVNET_LIST: ${{ secrets.RPC_URL_DEVNET_LIST }}
5260
RPC_URL_TESTNET_LIST: ${{ secrets.RPC_URL_TESTNET_LIST }}
5361
RPC_URL_LOCALNET_LIST: ${{ secrets.RPC_URL_LOCALNET_LIST }}
62+
RPC_WEB_SOCKET_URL_MAINNET: ${{ secrets.RPC_WEB_SOCKET_URL_MAINNET }}
63+
RPC_WEB_SOCKET_URL_DEVNET: ${{ secrets.RPC_WEB_SOCKET_URL_DEVNET }}
64+
RPC_WEB_SOCKET_URL_TESTNET: ${{ secrets.RPC_WEB_SOCKET_URL_TESTNET }}
65+
RPC_WEB_SOCKET_URL_LOCALNET: ${{ secrets.RPC_WEB_SOCKET_URL_LOCALNET }}
5466
EXPLORER_BASE_URL: ${{ secrets.EXPLORER_BASE_URL }}
5567
PRICE_API_BASE_URL: ${{ secrets.PRICE_API_BASE_URL }}
5668
LOCAL_API_BASE_URL: ${{ secrets.LOCAL_API_BASE_URL }}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Text as ChakraText, Flex, Stack } from '@chakra-ui/react';
22

33
import { OnProtocolRequest } from './OnProtocolRequest';
4+
import { WebSockets } from './WebSockets';
45

56
export const Handlers = () => (
67
<Flex direction="column" width="full" marginBottom="5">
@@ -9,6 +10,7 @@ export const Handlers = () => (
910
</ChakraText>
1011
<Stack direction="row" gap="5" wrap="wrap">
1112
<OnProtocolRequest />
13+
<WebSockets />
1214
</Stack>
1315
</Flex>
1416
);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { Button, Card, Flex } from '@chakra-ui/react';
3+
4+
import { TestDappRpcRequestMethod } from '../../../../snap/src/core/handlers/onRpcRequest/types';
5+
import { useInvokeSnap } from '../../hooks';
6+
import { useShowToasterForResponse } from '../../hooks/useToasterForResponse';
7+
8+
export const WebSockets = () => {
9+
const invokeSnap = useInvokeSnap();
10+
const { showToasterForResponse } = useShowToasterForResponse();
11+
12+
const setupAllConnections = async () => {
13+
showToasterForResponse(
14+
{ result: 'ok' },
15+
{
16+
title: 'Initiated WebSocket connections to all active networks',
17+
description: `Inspect the offscreen console`,
18+
},
19+
);
20+
21+
await invokeSnap({
22+
method: TestDappRpcRequestMethod.TestSetupAllConnections,
23+
});
24+
};
25+
26+
const subscribeToAccount = async () => {
27+
await invokeSnap({
28+
method: TestDappRpcRequestMethod.TestSubscribeToAccount,
29+
});
30+
31+
showToasterForResponse(
32+
{ result: 'ok' },
33+
{
34+
title:
35+
'Initiated subscription to account 8A4AptCThfbuknsbteHgGKXczfJpfjuVA9SLTSGaaLGC',
36+
description: `Inspect the offscreen console`,
37+
},
38+
);
39+
};
40+
41+
return (
42+
<Card.Root>
43+
<Card.Header>
44+
<Card.Title>WebSockets</Card.Title>
45+
</Card.Header>
46+
<Card.Body gap="2">
47+
<Flex>
48+
<Button
49+
variant="outline"
50+
onClick={setupAllConnections}
51+
marginRight="1"
52+
>
53+
Setup All Connections
54+
</Button>
55+
</Flex>
56+
<Flex>
57+
<Button
58+
variant="outline"
59+
onClick={subscribeToAccount}
60+
marginRight="1"
61+
>
62+
Subscribe to account
63+
</Button>
64+
</Flex>
65+
</Card.Body>
66+
</Card.Root>
67+
);
68+
};

packages/snap/.env.sample

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ RPC_URL_MAINNET_LIST=
1010
RPC_URL_DEVNET_LIST=
1111
RPC_URL_TESTNET_LIST=
1212
RPC_URL_LOCALNET_LIST=
13+
14+
RPC_WEB_SOCKET_URL_MAINNET=
15+
RPC_WEB_SOCKET_URL_DEVNET=
16+
RPC_WEB_SOCKET_URL_TESTNET=
17+
RPC_WEB_SOCKET_URL_LOCALNET=
18+
1319
EXPLORER_BASE_URL=
1420

1521
PRICE_API_BASE_URL=

packages/snap/snap.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ const environment = {
1111
RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST ?? '',
1212
RPC_URL_TESTNET_LIST: process.env.RPC_URL_TESTNET_LIST ?? '',
1313
RPC_URL_LOCALNET_LIST: process.env.RPC_URL_LOCALNET_LIST ?? '',
14+
RPC_WEB_SOCKET_URL_MAINNET: process.env.RPC_WEB_SOCKET_URL_MAINNET ?? '',
15+
RPC_WEB_SOCKET_URL_DEVNET: process.env.RPC_WEB_SOCKET_URL_DEVNET ?? '',
16+
RPC_WEB_SOCKET_URL_TESTNET: process.env.RPC_WEB_SOCKET_URL_TESTNET ?? '',
17+
RPC_WEB_SOCKET_URL_LOCALNET: process.env.RPC_WEB_SOCKET_URL_LOCALNET ?? '',
1418
EXPLORER_BASE_URL: process.env.EXPLORER_BASE_URL ?? '',
1519
PRICE_API_BASE_URL: process.env.PRICE_API_BASE_URL ?? '',
1620
TOKEN_API_BASE_URL: process.env.TOKEN_API_BASE_URL ?? '',

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": "U6qs3tA3phhscX54m+BAD5Mi1aBgT9p9g3qQ2qHrCsQ=",
10+
"shasum": "XSE/gfpj/gSu5aA9IihnjJ+IczHvXAdJN1Z/EegXk7w=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

0 commit comments

Comments
 (0)