This project is a front-end application for visualizing cryptocurrency order books, trade history, and price charts. It is built using React and Redux, and it connects to the Coinbase WebSocket feed to fetch real-time data.
- Order Book: Displays the current bids and asks for a selected currency pair, with options to aggregate the order book data.
- Trade History: Shows the recent trades for the selected currency pair.
- Price Chart: Plots real-time bids and asks on a chart, with the best bid and ask prices highlighted.
- Currency Pair Selector: Allows users to select different currency pairs to view their order books, trade history, and price charts.
- Environment Selector: Users can switch between the main net and test net environments.
- OrderBook: Displays the order book with bids and asks, and allows aggregation of data.
import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import LoadingBar from "./loadingBar";
import { aggregateOrderBook, calculateAggregationRange } from "../utils";
import "../styles/orderBook.css";
const DEFAULT_AGGREGATION_AMOUNT = 0.0;
const OrderBook
- TradeHistory: Shows the recent trade history for the selected currency pair.
import "../styles/tradeHistory.css";
import { useSelector } from "react-redux";
import { formatDate } from "../utils";
import { SYSTEM_ENVIRONMENT } from "../config";
import LoadingBar from "./loadingBar";
const TradeHistory = () => {
const { tradeHistory, isLoading } = useSelector(
(state) => state.tradeHistory
);
const { currencyPair, systemEnvironment } = useSelector(
(state) => state.orderBooks
);
return (
<div className="trade-history">
<h3>Trade History</h3>
<hr />
<div className="trade-history-header">
<h5 className="trade-history-header-element">Trade Size</h5>
<h5 className="trade-history-header-title-element trade-history-price">
Price ({currencyPair.split("-")[1]})
</h5>
<h5 className="trade-history-header-title-element">Time</h5>
</div>
{systemEnvironment === SYSTEM_ENVIRONMENT.MAIN_NET ? (
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
}}
>
<span style={{ color: "yellow" }}>Cannot display on Main_Net</span>
</div>
) : (
<>
{isLoading ? (
<div className="trade-history-loading">
<LoadingBar />
</div>
) : (
tradeHistory.map((trade, index) => (
<TradeHistoryRow key={`trade-${index}`} trade={trade} />
))
)}
</>
)}
</div>
);
};
- PriceChart: Plots real-time bids and asks on a chart.
import React, { useEffect, useState, useMemo } from "react";
import Plot from "react-plotly.js";
import { useSelector } from "react-redux";
import BestOrder from "./bestOrder";
import { formatDate } from "../utils";
import "../styles/priceChart.css";
const PriceChart
- CurrencyPairSelector: Dropdown to select different currency pairs.
import "../styles/currencyPair.css";
import React, { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { setCurrencyPair } from "../store/actions/orderBookActions";
import { DEFAULT_CURRENCY_PAIR } from "../config";
const AVAILABLE_CURRENCY = [
"ETH-USD",
"ETH-BTC",
"BTC-USD",
"LTC-USD",
"BCH-USD",
];
const CurrencyPairSelector = () => {
const [selectedPair, setSelectedPair] = useState(DEFAULT_CURRENCY_PAIR);
const dispatch = useDispatch();
const handleChange = (event) => {
setCurrencyPair(event.target.value);
setSelectedPair(event.target.value);
};
useEffect(() => {
dispatch(setCurrencyPair(selectedPair));
}, [selectedPair, dispatch]);
return (
<select value={selectedPair} onChange={handleChange}>
{AVAILABLE_CURRENCY.map((currency, key) => (
<option key={key} value={currency}>
{currency}
</option>
))}
</select>
);
};
export default CurrencyPairSelector;
- Header: Displays the connection status and allows switching between environments.
import { useSelector, useDispatch } from "react-redux";
import { setEnvironment } from "../store/actions/orderBookActions";
import { SYSTEM_ENVIRONMENT } from "../config";
export default function Header({ isConnected }) {
const dispatch = useDispatch();
const { systemEnvironment } = useSelector((state) => state.orderBooks);
const onTestNetChange = (e) => {
dispatch(
setEnvironment(
e.target.value === "TEST_NET"
? SYSTEM_ENVIRONMENT.TEST_NET
: SYSTEM_ENVIRONMENT.MAIN_NET
)
);
};
return (
<div
style={{
color: "white",
display: "flex",
justifyContent: "space-around",
alignItems: "center",
}}
>
<div className="connection-status">
{isConnected ? (
<div className="system-connected"></div>
) : (
<div className="system-disconnected"></div>
)}
<span>{isConnected ? "Connected" : "Not-Connected"}</span>
</div>
<h1>CoinRoutes Frontend Test</h1>
<select value={systemEnvironment} onChange={onTestNetChange}>
<option value="TEST_NET">Test Net</option>
<option value="MAIN_NET">Main Net</option>
</select>
</div>
);
}
- useWebSocket: Manages the WebSocket connection to the Coinbase feed, handles incoming messages, and updates the Redux store.
import { useEffect, useRef, useState, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getOrderBookSubMessage, getOrderBookUnSubMessage } from "../utils";
import {
setOrderBook,
setBestOrderBook,
resetOrderBook,
} from "../store/actions/orderBookActions";
import {
updateTradeHistory,
resetTradeHistory,
} from "../store/actions/tradeHistoryActions";
import Worker from "../workers/orderBookWorker?worker";
import TaskQueue from "../utils/taskQueue";
import {
COINBASE_SOCKET_URL,
COINBASE_SANDBOX_SOCKET_URL,
- Actions: Defines actions for setting the order book, trade history, currency pair, and environment.
export const SET_CURRENCY_PAIR = 'SET_CURRENCY_PAIR';
export const SET_ORDER_BOOK = 'SET_ORDER_BOOK';
export const SET_BEST_ORDER_BOOK = 'SET_BEST_ORDER_BOOK';
export const RESET_ORDER_BOOK = 'RESET_ORDER_BOOK';
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
export const setCurrencyPair = (currencyPair) => ({
type: SET_CURRENCY_PAIR,
payload: currencyPair,
});
export const setOrderBook = (orderBook) => ({
type: SET_ORDER_BOOK,
payload: orderBook,
});
export const setBestOrderBook = (bestOrderBook) => ({
type: SET_BEST_ORDER_BOOK,
payload: bestOrderBook,
});
export const resetOrderBook = () => ({
type: RESET_ORDER_BOOK,
});
export const setEnvironment = (environment) => ({
type: SET_ENVIRONMENT,
payload: environment
});
export const SET_TRADE_HISTORY = 'SET_TRADE_HISTORY';
export const UPDATE_TRADE_HISTORY = 'UPDATE_TRADE_HISTORY';
export const RESET_TRADE_HISTORY = 'RESET_TRADE_HISTORY';
export const setTradeHistory = (tradeHistory) => ({
type: SET_TRADE_HISTORY,
payload: tradeHistory,
});
export const updateTradeHistory = (trade) => ({
type: UPDATE_TRADE_HISTORY,
payload: trade,
});
export const resetTradeHistory = () => ({
type: RESET_TRADE_HISTORY,
});
- Reducers: Handles state changes for the order book and trade history.
import { SET_ORDER_BOOK, SET_CURRENCY_PAIR, RESET_ORDER_BOOK, SET_BEST_ORDER_BOOK, SET_ENVIRONMENT } from '../actions/orderBookActions';
import { DEFAULT_CURRENCY_PAIR, SYSTEM_ENVIRONMENT } from '../../config';
const MAX_ORDERS_LIST = 50;
const initialState = {
bids: [],
asks: [],
bestAsk: {
size: 0,
amount: 0
},
bestBid: {
size: 0,
amount: 0
},
currencyPair: DEFAULT_CURRENCY_PAIR,
isLoading: true,
systemEnvironment: SYSTEM_ENVIRONMENT.MAIN_NET
};
const orderBookReducer = (state = initialState, action) => {
switch (action.type) {
case SET_ORDER_BOOK:
const bids = action.payload.bids;
const asks = action.payload.asks;
if (bids.length > MAX_ORDERS_LIST) {
bids.length = MAX_ORDERS_LIST;
}
if (asks.length > MAX_ORDERS_LIST) {
asks.length = MAX_ORDERS_LIST;
}
return {
...state,
isLoading: false,
bids: bids,
asks: asks,
};
case SET_CURRENCY_PAIR:
return {
...state,
isLoading: false,
currencyPair: action.payload,
};
case RESET_ORDER_BOOK:
const currency = state.currencyPair;
const systemEnvironment = state.systemEnvironment;
return {
...initialState,
isLoading: false,
systemEnvironment: systemEnvironment,
currencyPair: currency
};
case SET_BEST_ORDER_BOOK:
const bestAsk = {
size: action.payload.best_ask_size,
amount: action.payload.best_ask
}
const bestBid = {
size: action.payload.best_bid_size,
amount: action.payload.best_bid
}
return { ...state, bestAsk, bestBid };
case SET_ENVIRONMENT:
return {
...state,
systemEnvironment: action.payload
}
default:
return state;
}
};
import { SET_TRADE_HISTORY, UPDATE_TRADE_HISTORY, RESET_TRADE_HISTORY } from '../actions/tradeHistoryActions';
const MAX_TRADE_HISTORY_LENGTH = 32;
const initialState = {
tradeHistory: [],
isLoading: true,
};
const tradeHistoryReducer = (state = initialState, action) => {
switch (action.type) {
case SET_TRADE_HISTORY:
return {
...state,
isLoading: false,
tradeHistory: action.payload,
};
case UPDATE_TRADE_HISTORY:
return {
...state,
isLoading: false,
tradeHistory: [action.payload, ...state.tradeHistory].slice(0, MAX_TRADE_HISTORY_LENGTH), //keep the recent history with length of [MAX_TRADE_HISTORY_LENGTH]
};
case RESET_TRADE_HISTORY:
return {
...initialState,
};
default:
return state;
}
};
export default tradeHistoryReducer;
- Store: Configures the Redux store with the root reducer.
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ immutableCheck: true, serializableCheck: true }),
});
- orderBook.utils.js: Contains utility functions for processing order book data.
import { getWebSocketAuth } from './general.utils';
import { SYSTEM_ENVIRONMENT } from '../config';
const apiKey = import.meta.env.VITE_COINBASE_API_KEY;
const passphrase = import.meta.env.VITE_COINBASE_PASSPHRASE;
export function getOrderBookSubMessage({ currencyPair, channels = ['level2_batch', 'ticker'], environment = SYSTEM_ENVIRONMENT.TEST_NET }) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = getWebSocketAuth(timestamp);
let subscription = {
type: 'subscribe',
product_ids: [currencyPair],
channels: [...channels],
};
if (environment === SYSTEM_ENVIRONMENT.MAIN_NET) {
return JSON.stringify(subscription);
}
return JSON.stringify({
type: 'subscribe',
product_ids: [currencyPair],
channels: [...channels, 'full'],
signature: signature,
key: apiKey,
passphrase: passphrase,
timestamp: timestamp,
});
}
export function getOrderBookUnSubMessage({ currencyPair, channels = ['level2_batch', 'ticker'] }) {
return JSON.stringify({
type: "unsubscribe",
product_ids: [currencyPair],
channels: channels
});
}
- general.utils.js: Contains general utility functions, including WebSocket authentication.
import CryptoJS from 'crypto-js';
export function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString("en-GB", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
};
export function getWebSocketAuth(timestamp) {
const apiSecret = import.meta.env.VITE_COINBASE_API_SECRET;
const method = 'GET';
const requestPath = '/users/self/verify';
const body = '';
const prehash = timestamp + method + requestPath + body;
const key = CryptoJS.enc.Base64.parse(apiSecret);
const hmac = CryptoJS.HmacSHA256(prehash, key);
return CryptoJS.enc.Base64.stringify(hmac);
};
export function padDecimals(numbers) {
let maxDecimals = 0;
const decimals = numbers.toString().split('.')[1];
if (decimals && decimals.length > maxDecimals) {
maxDecimals = decimals.length;
}
return decimals.toFixed(maxDecimals);
}
- orderBookWorker.js: Web worker for processing order book data in the background.
import { createOrderBooksFromList, filterNewBooks } from '../utils/orderBook.utils';
self.onmessage = function (e) {
const message = e.data;
const timestamp = new Date(message.time).getTime();
//if initial data
if (message.type == "snapshot") {
const asks = createOrderBooksFromList(message.asks, timestamp, true).reverse();
const bids = createOrderBooksFromList(message.bids, timestamp);
self.postMessage({ asks: asks, bids: bids });
//If an update message
} else if (message.type == 'l2update') {
let newBids = message.changes.filter((order) => order[0] == 'buy').map((ask) => createBookObject(ask, timestamp));
let newAsks = message.changes.filter((order) => order[0] == 'sell').map((bid) => createBookObject(bid, timestamp));
const { updatedBids, updatedAsks } = filterNewBooks(message.currentAsks, message.currentBids, newAsks, newBids);
self.postMessage({ asks: updatedAsks.reverse(), bids: updatedBids });
}
};
function createBookObject(book, timestamp) {
return { price: book[1], marketSize: book[2], timestamp: timestamp }
}
- Node.js and npm installed on your machine.
-
Clone the repository:
git clone https://github.com/mr-muhammad-rehan/coinRoutes-frontend.git
-
Navigate to the project directory:
cd coinRoutes-frontend
-
Install the dependencies:
npm install
-
Start the development server:
npm run dev
Once the development server is running, you can access the application in your web browser at http://localhost:3000
.
This explanation should provide a comprehensive overview of the project, its structure, and how to get started. Feel free to modify it as needed!