Skip to content

Commit e2f46ef

Browse files
huntiefacebook-github-bot
authored andcommitted
Add Binance API WebSocket demo
Summary: NOTE: **Optional** part of this stack. Adds an additional WebSocket demo to RNTester, displaying a feed of live trades from the open `stream.binance.com` WS API. This is nice as it avoids the need to run the local WS server (other demo) and continuously updates every second without additional interaction. It's a little more self-apparent as a user demo also (e.g. blog post / docs). Changelog: [Internal] Differential Revision: D111561997
1 parent 9f3108d commit e2f46ef

3 files changed

Lines changed: 349 additions & 0 deletions

File tree

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
12+
13+
import RNTesterText from '../../components/RNTesterText';
14+
import * as React from 'react';
15+
import {useCallback, useEffect, useRef, useState} from 'react';
16+
import {FlatList, Pressable, StyleSheet, View} from 'react-native';
17+
18+
const STREAM_URL = 'wss://stream.binance.com:9443/ws';
19+
const MAX_LOG_ENTRIES = 30;
20+
21+
type TradeSymbol = 'btcusdt' | 'ethusdt' | 'solusdt';
22+
type ConnectionState = 'idle' | 'connecting' | 'open' | 'closed';
23+
type FrameLogEntry = Readonly<{
24+
id: string,
25+
direction: 'sent' | 'received',
26+
time: string,
27+
summary: string,
28+
}>;
29+
30+
const SYMBOLS: ReadonlyArray<TradeSymbol> = ['btcusdt', 'ethusdt', 'solusdt'];
31+
32+
const STATE_COLOR = {
33+
idle: '#999999',
34+
connecting: '#e6a700',
35+
open: '#2e7d32',
36+
closed: '#c62828',
37+
};
38+
39+
function nowTime(): string {
40+
const d = new Date();
41+
const pad = (n: number): string => String(n).padStart(2, '0');
42+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
43+
}
44+
45+
component BinanceWebSocketDemo() {
46+
const [state, setState] = useState<ConnectionState>('idle');
47+
const [symbol, setSymbol] = useState<TradeSymbol>('btcusdt');
48+
const [price, setPrice] = useState<?string>(null);
49+
const [log, setLog] = useState<Array<FrameLogEntry>>([]);
50+
const wsRef = useRef<?WebSocket>(null);
51+
const idRef = useRef<number>(0);
52+
53+
const appendLog = useCallback(
54+
(direction: 'sent' | 'received', summary: string) => {
55+
idRef.current += 1;
56+
const entry: FrameLogEntry = {
57+
id: String(idRef.current),
58+
direction,
59+
time: nowTime(),
60+
summary,
61+
};
62+
setLog(prev => [entry, ...prev].slice(0, MAX_LOG_ENTRIES));
63+
},
64+
[],
65+
);
66+
67+
const send = useCallback(
68+
(method: 'SUBSCRIBE' | 'UNSUBSCRIBE', target: TradeSymbol) => {
69+
const ws = wsRef.current;
70+
if (ws == null || ws.readyState !== WebSocket.OPEN) {
71+
return;
72+
}
73+
idRef.current += 1;
74+
ws.send(
75+
JSON.stringify({
76+
method,
77+
params: [`${target}@trade`],
78+
id: idRef.current,
79+
}),
80+
);
81+
appendLog('sent', `${method} ${target}@trade`);
82+
},
83+
[appendLog],
84+
);
85+
86+
const connect = useCallback(() => {
87+
if (wsRef.current != null) {
88+
return;
89+
}
90+
setState('connecting');
91+
setLog([]);
92+
const ws = new WebSocket(STREAM_URL);
93+
wsRef.current = ws;
94+
95+
ws.onopen = () => {
96+
setState('open');
97+
send('SUBSCRIBE', symbol);
98+
};
99+
100+
ws.onmessage = event => {
101+
try {
102+
const data: {e?: string, s?: string, p?: string} = JSON.parse(
103+
String(event.data),
104+
);
105+
const tradePrice = data.p;
106+
if (data.e === 'trade' && tradePrice != null) {
107+
setPrice(tradePrice);
108+
appendLog('received', `trade ${data.s ?? ''} @ ${tradePrice}`);
109+
} else {
110+
appendLog('received', String(event.data).slice(0, 80));
111+
}
112+
} catch (error: unknown) {
113+
appendLog('received', String(event.data).slice(0, 80));
114+
}
115+
};
116+
117+
ws.onerror = () => {
118+
appendLog('received', 'error event');
119+
};
120+
121+
ws.onclose = () => {
122+
setState('closed');
123+
wsRef.current = null;
124+
};
125+
}, [appendLog, send, symbol]);
126+
127+
const disconnect = useCallback(() => {
128+
wsRef.current?.close();
129+
}, []);
130+
131+
const changeSymbol = useCallback(
132+
(next: TradeSymbol) => {
133+
if (next === symbol) {
134+
return;
135+
}
136+
send('UNSUBSCRIBE', symbol);
137+
send('SUBSCRIBE', next);
138+
setSymbol(next);
139+
setPrice(null);
140+
},
141+
[send, symbol],
142+
);
143+
144+
useEffect(() => {
145+
return () => {
146+
wsRef.current?.close();
147+
};
148+
}, []);
149+
150+
const busy = state === 'connecting' || state === 'open';
151+
152+
return (
153+
<View style={styles.container}>
154+
<View style={styles.statusRow}>
155+
<View
156+
style={[styles.statusDot, {backgroundColor: STATE_COLOR[state]}]}
157+
/>
158+
<RNTesterText style={styles.statusText}>
159+
{state.toUpperCase()}
160+
</RNTesterText>
161+
</View>
162+
163+
<RNTesterText variant="label" style={styles.symbolLabel}>
164+
{symbol.toUpperCase()}
165+
</RNTesterText>
166+
<RNTesterText style={styles.price}>
167+
{price != null ? `$${Number(price).toFixed(6)}` : '—'}
168+
</RNTesterText>
169+
170+
<View style={styles.symbolRow}>
171+
{SYMBOLS.map(s => (
172+
<Pressable
173+
key={s}
174+
onPress={() => changeSymbol(s)}
175+
style={[
176+
styles.symbolButton,
177+
s === symbol && styles.symbolButtonActive,
178+
]}>
179+
<RNTesterText
180+
style={[
181+
styles.symbolButtonText,
182+
s === symbol && styles.symbolButtonTextActive,
183+
]}>
184+
{s.replace('usdt', '').toUpperCase()}
185+
</RNTesterText>
186+
</Pressable>
187+
))}
188+
</View>
189+
190+
<View style={styles.controlsRow}>
191+
<Pressable
192+
style={[styles.controlButton, busy && styles.controlButtonDisabled]}
193+
disabled={busy}
194+
onPress={connect}>
195+
<RNTesterText style={styles.controlButtonText}>Connect</RNTesterText>
196+
</Pressable>
197+
<Pressable
198+
style={[
199+
styles.controlButton,
200+
state !== 'open' && styles.controlButtonDisabled,
201+
]}
202+
disabled={state !== 'open'}
203+
onPress={disconnect}>
204+
<RNTesterText style={styles.controlButtonText}>
205+
Disconnect
206+
</RNTesterText>
207+
</Pressable>
208+
</View>
209+
210+
<RNTesterText variant="label" style={styles.logHeader}>
211+
Frame log ({log.length})
212+
</RNTesterText>
213+
<FlatList
214+
style={styles.log}
215+
data={log}
216+
keyExtractor={item => item.id}
217+
renderItem={({item}) => (
218+
<RNTesterText style={styles.logLine}>
219+
<RNTesterText
220+
style={item.direction === 'sent' ? styles.sent : styles.received}>
221+
{item.direction === 'sent' ? '↑ ' : '↓ '}
222+
</RNTesterText>
223+
{item.time}{item.summary}
224+
</RNTesterText>
225+
)}
226+
/>
227+
</View>
228+
);
229+
}
230+
231+
const styles = StyleSheet.create({
232+
container: {
233+
flex: 1,
234+
padding: 16,
235+
},
236+
statusRow: {
237+
flexDirection: 'row',
238+
alignItems: 'center',
239+
marginBottom: 8,
240+
},
241+
statusDot: {
242+
width: 10,
243+
height: 10,
244+
borderRadius: 5,
245+
marginRight: 6,
246+
},
247+
statusText: {
248+
fontSize: 12,
249+
fontWeight: '600',
250+
letterSpacing: 0.5,
251+
},
252+
symbolLabel: {
253+
fontSize: 14,
254+
},
255+
price: {
256+
fontSize: 40,
257+
fontWeight: '700',
258+
marginBottom: 16,
259+
},
260+
symbolRow: {
261+
flexDirection: 'row',
262+
marginBottom: 16,
263+
},
264+
symbolButton: {
265+
paddingVertical: 6,
266+
paddingHorizontal: 14,
267+
borderRadius: 16,
268+
borderWidth: 1,
269+
borderColor: '#cccccc',
270+
marginRight: 8,
271+
},
272+
symbolButtonActive: {
273+
backgroundColor: '#1e88e5',
274+
borderColor: '#1e88e5',
275+
},
276+
symbolButtonText: {
277+
fontSize: 13,
278+
},
279+
symbolButtonTextActive: {
280+
color: '#ffffff',
281+
fontWeight: '600',
282+
},
283+
controlsRow: {
284+
flexDirection: 'row',
285+
marginBottom: 16,
286+
},
287+
controlButton: {
288+
paddingVertical: 8,
289+
paddingHorizontal: 12,
290+
backgroundColor: '#f0f0f0',
291+
borderRadius: 6,
292+
marginRight: 8,
293+
},
294+
controlButtonDisabled: {
295+
opacity: 0.5,
296+
},
297+
controlButtonText: {
298+
fontSize: 13,
299+
fontWeight: '600',
300+
color: '#333333',
301+
},
302+
logHeader: {
303+
fontSize: 13,
304+
fontWeight: '600',
305+
marginBottom: 4,
306+
},
307+
log: {
308+
flex: 1,
309+
backgroundColor: '#fafafa',
310+
borderRadius: 6,
311+
padding: 8,
312+
},
313+
logLine: {
314+
fontSize: 12,
315+
fontFamily: 'Menlo',
316+
marginBottom: 4,
317+
color: '#333333',
318+
},
319+
sent: {
320+
color: '#1e88e5',
321+
fontWeight: '700',
322+
},
323+
received: {
324+
color: '#2e7d32',
325+
fontWeight: '700',
326+
},
327+
});
328+
329+
exports.title = 'WebSocket (Binance demo)';
330+
exports.category = 'Basic';
331+
exports.description =
332+
'Live Binance trade ticker over a public WebSocket stream.';
333+
exports.examples = [
334+
{
335+
title: 'Binance live trade ticker',
336+
render(): React.Node {
337+
return <BinanceWebSocketDemo />;
338+
},
339+
},
340+
] as Array<RNTesterModuleExample>;

packages/rn-tester/js/utils/RNTesterList.android.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ const APIs: Array<RNTesterModuleInfo> = (
356356
category: 'Basic',
357357
module: require('../examples/Vibration/VibrationExample'),
358358
},
359+
{
360+
key: 'BinanceWebSocketDemo',
361+
category: 'Basic',
362+
module: require('../examples/WebSocket/BinanceWebSocketDemo'),
363+
},
359364
{
360365
key: 'WebSocketExample',
361366
category: 'Basic',

packages/rn-tester/js/utils/RNTesterList.ios.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@ const APIs: Array<RNTesterModuleInfo> = (
344344
key: 'VibrationExample',
345345
module: require('../examples/Vibration/VibrationExample'),
346346
},
347+
{
348+
key: 'BinanceWebSocketDemo',
349+
module: require('../examples/WebSocket/BinanceWebSocketDemo'),
350+
},
347351
{
348352
key: 'WebSocketExample',
349353
module: require('../examples/WebSocket/WebSocketExample'),

0 commit comments

Comments
 (0)