11import {
22 MethodNotFoundError ,
33 type OnRpcRequestHandler ,
4+ type OnWebSocketEventHandler ,
45} from '@metamask/snaps-sdk' ;
56import { assert } from '@metamask/utils' ;
67
7- import type { FetchParams } from './types' ;
8+ import type { UrlParams } from './types' ;
9+
10+ const DEFAULT_WEBSOCKET_URL = 'ws://localhost:8545' ;
811
912/**
1013 * Fetch a JSON file from the provided URL. This uses the standard `fetch`
@@ -16,21 +19,78 @@ import type { FetchParams } from './types';
1619 *
1720 * @param url - The URL to fetch the data from. This function assumes that the
1821 * provided URL is a JSON document.
19- * @returns There response as JSON.
22+ * @returns The response as JSON.
2023 * @throws If the provided URL is not a JSON document.
2124 */
2225async function getJson ( url : string ) {
2326 const response = await fetch ( url ) ;
2427 return await response . json ( ) ;
2528}
2629
30+ /**
31+ * Open a WebSocket connection to a local Ethereum node and subscribe to
32+ * block updates.
33+ *
34+ * @param url - The URL of the WebSocket connection.
35+ * @returns Null.
36+ * @throws If the WebSocket connection fails to open.
37+ */
38+ async function subscribe ( url : string = DEFAULT_WEBSOCKET_URL ) {
39+ const id = await snap . request ( {
40+ method : 'snap_openWebSocket' ,
41+ params : {
42+ url,
43+ } ,
44+ } ) ;
45+
46+ const message = JSON . stringify ( {
47+ jsonrpc : '2.0' ,
48+ id : 1 ,
49+ method : 'eth_subscribe' ,
50+ params : [ 'newHeads' ] ,
51+ } ) ;
52+
53+ return await snap . request ( {
54+ method : 'snap_sendWebSocketMessage' ,
55+ params : { id, message } ,
56+ } ) ;
57+ }
58+
59+ /**
60+ * Close a WebSocket connection to a local Ethereum node, if it exists.
61+ *
62+ * @param url - The URL of the WebSocket connection.
63+ * @returns Null.
64+ */
65+ async function unsubscribe ( url : string = DEFAULT_WEBSOCKET_URL ) {
66+ const sockets = await snap . request ( {
67+ method : 'snap_getWebSockets' ,
68+ } ) ;
69+
70+ const socket = sockets . find ( ( socketInfo ) => socketInfo . url === url ) ;
71+
72+ if ( ! socket ) {
73+ return null ;
74+ }
75+
76+ return await snap . request ( {
77+ method : 'snap_closeWebSocket' ,
78+ params : { id : socket . id } ,
79+ } ) ;
80+ }
81+
2782/**
2883 * Handle incoming JSON-RPC requests from the dapp, sent through the
29- * `wallet_invokeSnap` method. This handler handles a single method :
84+ * `wallet_invokeSnap` method. This handler handles four methods :
3085 *
3186 * - `fetch`: Fetch a JSON document from the provided URL. This demonstrates
3287 * that a snap can make network requests through the `fetch` function. Note that
3388 * the `endowment:network-access` permission must be enabled for this to work.
89+ * - `startWebSocket`: Open a WebSocket connection to a local Ethereum node
90+ * and subscribe to block updates.
91+ * - `closeWebSocket`: Close a WebSocket connection, if one exists.
92+ * - `getState`: Get the state of the Snap, including the block number and whether
93+ * the WebSocket connection is active.
3494 *
3595 * @param params - The request parameters.
3696 * @param params.request - The JSON-RPC request object.
@@ -40,14 +100,77 @@ async function getJson(url: string) {
40100 * @see https://docs.metamask.io/snaps/reference/permissions/#endowmentnetwork-access
41101 */
42102export const onRpcRequest : OnRpcRequestHandler = async ( { request } ) => {
103+ const params = request . params as UrlParams | undefined ;
104+ const url = params ?. url ;
105+
43106 switch ( request . method ) {
44107 case 'fetch' : {
45- const params = request . params as FetchParams | undefined ;
46- assert ( params ?. url , 'Required url parameter was not specified.' ) ;
47- return await getJson ( params . url ) ;
108+ assert ( url , 'Required url parameter was not specified.' ) ;
109+ return await getJson ( url ) ;
110+ }
111+
112+ case 'startWebSocket' :
113+ return subscribe ( url ) ;
114+
115+ case 'stopWebSocket' :
116+ return unsubscribe ( url ) ;
117+
118+ case 'getState' : {
119+ const state = await snap . request ( {
120+ method : 'snap_getState' ,
121+ params : { encrypted : false } ,
122+ } ) ;
123+ return state ?? { blockNumber : null , open : false } ;
48124 }
49125
50126 default :
51127 throw new MethodNotFoundError ( { method : request . method } ) ;
52128 }
53129} ;
130+
131+ /**
132+ * Handle incoming WebSocket events sent by a client.
133+ *
134+ * @param params - The request parameters.
135+ * @param params.event - The WebSocket event.
136+ * @returns Nothing.
137+ */
138+ export const onWebSocketEvent : OnWebSocketEventHandler = async ( { event } ) => {
139+ const { origin } = event ;
140+
141+ if ( event . type === 'open' ) {
142+ await snap . request ( {
143+ method : 'snap_setState' ,
144+ params : {
145+ value : { blockNumber : null , origin, open : true } ,
146+ encrypted : false ,
147+ } ,
148+ } ) ;
149+ return ;
150+ }
151+
152+ if ( event . type === 'close' ) {
153+ await snap . request ( {
154+ method : 'snap_setState' ,
155+ params : {
156+ value : { blockNumber : null , origin : null , open : false } ,
157+ encrypted : false ,
158+ } ,
159+ } ) ;
160+ return ;
161+ }
162+
163+ assert ( event . data . type === 'text' ) ;
164+ const json = JSON . parse ( event . data . message ) ;
165+
166+ if ( ! json . params ?. result ) {
167+ return ;
168+ }
169+
170+ const blockNumber = parseInt ( json . params . result . number . slice ( 2 ) , 16 ) ;
171+
172+ await snap . request ( {
173+ method : 'snap_setState' ,
174+ params : { key : 'blockNumber' , value : blockNumber , encrypted : false } ,
175+ } ) ;
176+ } ;
0 commit comments