From 815c3c3ca13a8a58e59888b5265f931f6d2b8589 Mon Sep 17 00:00:00 2001 From: wphan Date: Mon, 2 Dec 2024 17:38:18 -0800 Subject: [PATCH] add /debug to dlobPublisher --- .gitignore | 6 +- README.md | 20 +++- redisCluster.sh | 142 ++++++++++++++++++++++++ src/dlob-subscriber/DLOBSubscriberIO.ts | 9 +- src/publishers/dlobPublisher.ts | 49 +++++++- src/utils/utils.ts | 15 +++ yarn.lock | 16 ++- 7 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 redisCluster.sh diff --git a/.gitignore b/.gitignore index acde1c6..d5455f7 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,8 @@ src/**.js.map lib src/playground.ts -*dump.rdb \ No newline at end of file +# redis files +*.rdb +*.log +*.confg + diff --git a/README.md b/README.md index 5caadcc..6126065 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ This is the backend server that provides a REST API for the drift [DLOB](https:/ ## Setup +The build dependencies +``` +git submodule update --init +bash build_all.sh +``` + First set the necessary environment variables: ``` cp .env.example .env @@ -66,18 +72,28 @@ To run the websocket server, a Redis cache is required, and the following enviro * `REDIS_PASSWORDS` * `REDIS_PORTS` -In one terminal, run: +In the first terminal, start the redis cluster: +``` +bash redisCluster.sh start +bash redisCluster.sh create +``` + +In second terminal, run: ``` yarn run dlob-publisher ``` -In a second terminal, run: +In a third terminal, run: ``` yarn run ws-manager ``` Then connect to the ws server at ws://127.0.0.1:3000 +When you're done, stop the redis cluster: +``` +bash redisCluster.sh stop +``` # Run the example client diff --git a/redisCluster.sh b/redisCluster.sh new file mode 100644 index 0000000..3976d17 --- /dev/null +++ b/redisCluster.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +# Settings +CLUSTER_HOST=127.0.0.1 +PORT=6378 +TIMEOUT=2000 +NODES=6 +REPLICAS=1 +PROTECTED_MODE=yes +ADDITIONAL_OPTIONS="" + +# You may want to put the above config parameters into config.sh in order to +# override the defaults without modifying this script. + +if [ -a config.sh ] +then + source "config.sh" +fi + +# Computed vars +ENDPORT=$((PORT+NODES)) + +if [ "$1" == "start" ] +then + while [ $((PORT < ENDPORT)) != "0" ]; do + PORT=$((PORT+1)) + echo "Starting $PORT" + redis-server --port $PORT --protected-mode $PROTECTED_MODE --cluster-enabled yes --cluster-config-file nodes-${PORT}.conf --cluster-node-timeout $TIMEOUT --appendonly yes --appendfilename appendonly-${PORT}.aof --appenddirname appendonlydir-${PORT} --dbfilename dump-${PORT}.rdb --logfile ${PORT}.log --daemonize yes ${ADDITIONAL_OPTIONS} + done + exit 0 +fi + +if [ "$1" == "create" ] +then + HOSTS="" + while [ $((PORT < ENDPORT)) != "0" ]; do + PORT=$((PORT+1)) + HOSTS="$HOSTS $CLUSTER_HOST:$PORT" + done + OPT_ARG="" + if [ "$2" == "-f" ]; then + OPT_ARG="--cluster-yes" + fi + redis-cli --cluster create $HOSTS --cluster-replicas $REPLICAS $OPT_ARG + exit 0 +fi + +if [ "$1" == "stop" ] +then + while [ $((PORT < ENDPORT)) != "0" ]; do + PORT=$((PORT+1)) + echo "Stopping $PORT" + redis-cli -p $PORT shutdown nosave + done + exit 0 +fi + +if [ "$1" == "restart" ] +then + OLD_PORT=$PORT + while [ $((PORT < ENDPORT)) != "0" ]; do + PORT=$((PORT+1)) + echo "Stopping $PORT" + redis-cli -p $PORT shutdown nosave + done + PORT=$OLD_PORT + while [ $((PORT < ENDPORT)) != "0" ]; do + PORT=$((PORT+1)) + echo "Starting $PORT" + redis-server --port $PORT --protected-mode $PROTECTED_MODE --cluster-enabled yes --cluster-config-file nodes-${PORT}.conf --cluster-node-timeout $TIMEOUT --appendonly yes --appendfilename appendonly-${PORT}.aof --appenddirname appendonlydir-${PORT} --dbfilename dump-${PORT}.rdb --logfile ${PORT}.log --daemonize yes ${ADDITIONAL_OPTIONS} + done + exit 0 +fi + +if [ "$1" == "watch" ] +then + PORT=$((PORT+1)) + while [ 1 ]; do + clear + date + redis-cli -p $PORT cluster nodes | head -30 + sleep 1 + done + exit 0 +fi + +if [ "$1" == "tail" ] +then + INSTANCE=$2 + PORT=$((PORT+INSTANCE)) + tail -f ${PORT}.log + exit 0 +fi + +if [ "$1" == "tailall" ] +then + tail -f *.log + exit 0 +fi + +if [ "$1" == "call" ] +then + while [ $((PORT < ENDPORT)) != "0" ]; do + PORT=$((PORT+1)) + redis-cli -p $PORT $2 $3 $4 $5 $6 $7 $8 $9 + done + exit 0 +fi + +if [ "$1" == "clean" ] +then + echo "Cleaning *.log" + rm -rf *.log + echo "Cleaning appendonlydir-*" + rm -rf appendonlydir-* + echo "Cleaning dump-*.rdb" + rm -rf dump-*.rdb + echo "Cleaning nodes-*.conf" + rm -rf nodes-*.conf + exit 0 +fi + +if [ "$1" == "clean-logs" ] +then + echo "Cleaning *.log" + rm -rf *.log + exit 0 +fi + +echo "Usage: $0 [start|create|stop|restart|watch|tail|tailall|clean|clean-logs|call]" +echo "start -- Launch Redis Cluster instances." +echo "create [-f] -- Create a cluster using redis-cli --cluster create." +echo "stop -- Stop Redis Cluster instances." +echo "restart -- Restart Redis Cluster instances." +echo "watch -- Show CLUSTER NODES output (first 30 lines) of first node." +echo "tail -- Run tail -f of instance at base port + ID." +echo "tailall -- Run tail -f for all the log files at once." +echo "clean -- Remove all instances data, logs, configs." +echo "clean-logs -- Remove just instances logs." +echo "call -- Call a command (up to 7 arguments) on all nodes." diff --git a/src/dlob-subscriber/DLOBSubscriberIO.ts b/src/dlob-subscriber/DLOBSubscriberIO.ts index 0a603f9..c988219 100644 --- a/src/dlob-subscriber/DLOBSubscriberIO.ts +++ b/src/dlob-subscriber/DLOBSubscriberIO.ts @@ -247,7 +247,7 @@ export class DLOBSubscriberIO extends DLOBSubscriber { l2Formatted['marketSlot'] !== lastMarketSlotAndTime.slot ) { logger.warn( - `Updating market slot for ${marketArgs.marketName} with slot ${l2Formatted['marketSlot']}` + `Updating market slot for ${marketArgs.marketName} from ${lastMarketSlotAndTime.slot} -> ${l2Formatted['marketSlot']}` ); this.lastMarketSlotMap .get(marketArgs.marketType) @@ -262,6 +262,13 @@ export class DLOBSubscriberIO extends DLOBSubscriber { asks: l2Formatted.asks.slice(0, 100), }); + console.log( + `bbo: ${l2Formatted['marketName']}`, + (+l2Formatted.bids[0].price / 1e6).toFixed(4), + '//', + (+l2Formatted.asks[0].price / 1e6).toFixed(4) + ); + this.redisClient.publish( `${clientPrefix}orderbook_${marketType}_${marketArgs.marketIndex}`, l2Formatted diff --git a/src/publishers/dlobPublisher.ts b/src/publishers/dlobPublisher.ts index c5f5037..a44d260 100644 --- a/src/publishers/dlobPublisher.ts +++ b/src/publishers/dlobPublisher.ts @@ -18,6 +18,8 @@ import { PerpMarketConfig, SpotMarketConfig, PhoenixSubscriber, + MarketType, + OraclePriceData, } from '@drift-labs/sdk'; import { RedisClient, RedisClientPrefix } from '@drift/common/clients'; @@ -25,6 +27,7 @@ import { logger, setLogLevel } from '../utils/logger'; import { SubscriberLookup, getOpenbookSubscriber, + l2WithBNToStrings, parsePositiveIntArray, sleep, } from '../utils/utils'; @@ -40,7 +43,7 @@ import { } from '../dlobProvider'; import FEATURE_FLAGS from '../utils/featureFlags'; import { GeyserOrderSubscriber } from '../grpc/OrderSubscriberGRPC'; -import express from 'express'; +import express, { Response, Request } from 'express'; import { handleHealthCheck } from '../core/metrics'; import { setGlobalDispatcher, Agent } from 'undici'; @@ -453,6 +456,50 @@ const main = async () => { } }; + const handleDebug = async (req: Request, res: Response) => { + const marketIndex = +req.query.marketIndex; + let marketType: MarketType = MarketType.PERP; + let oraclePriceData: OraclePriceData; + if (req.query.marketType === 'spot') { + marketType = MarketType.SPOT; + oraclePriceData = driftClient.getOracleDataForSpotMarket(marketIndex); + } else { + oraclePriceData = driftClient.getOracleDataForPerpMarket(marketIndex); + } + try { + const slot = slotSource.getSlot(); + const dlob = await dlobProvider.getDLOB(slot); + const l2 = dlob.getL2({ + marketIndex, + marketType, + depth: 5, + slot, + oraclePriceData, + }); + const l3 = dlob.getL3({ + marketIndex, + marketType, + slot, + oraclePriceData, + }); + const state = { + dlobSize: dlobProvider.size(), + slot, + markets: { + perp: perpMarketInfos, + spot: spotMarketInfos, + }, + l2: l2WithBNToStrings(l2), + l3, + }; + + res.json(state); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }; + app.get('/debug', handleDebug); + app.get( '/health', handleHealthCheck(WS_FALLBACK_FETCH_INTERVAL, dlobProvider) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d984f78..82fb44d 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -37,6 +37,21 @@ export const l2WithBNToStrings = (l2: L2OrderBook): any => { return l2; }; +export const l3WithBNToStrings = (l3: L3OrderBook): any => { + for (const key of Object.keys(l3)) { + for (const idx in l3[key]) { + const level = l3[key][idx]; + l3[key][idx] = { + price: level.price.toString(), + size: level.size.toString(), + maker: level.maker.toBase58(), + orderId: level.orderId.toString(), + }; + } + } + return l3; +}; + export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/yarn.lock b/yarn.lock index d616eaf..f8475ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -460,11 +460,12 @@ kuler "^2.0.0" "@drift-labs/sdk@file:./drift-common/protocol/sdk", "@drift-labs/sdk@file:drift-common/protocol/sdk": - version "2.96.0-beta.25" + version "2.103.0-beta.8" dependencies: "@coral-xyz/anchor" "0.28.0" "@coral-xyz/anchor-30" "npm:@coral-xyz/anchor@0.30.1" "@ellipsis-labs/phoenix-sdk" "^1.4.2" + "@grpc/grpc-js" "^1.8.0" "@openbook-dex/openbook-v2" "0.2.10" "@project-serum/serum" "^0.13.38" "@pythnetwork/client" "2.5.3" @@ -473,18 +474,20 @@ "@solana/spl-token" "0.3.7" "@solana/web3.js" "1.92.3" "@switchboard-xyz/on-demand" "1.2.42" + "@triton-one/yellowstone-grpc" "0.6.0" anchor-bankrun "^0.3.0" node-cache "^5.1.2" rpc-websockets "7.5.1" solana-bankrun "^0.3.0" strict-event-emitter-types "^2.0.0" + tweetnacl "1.0.3" uuid "^8.3.2" zstddec "^0.1.0" "@drift/common@file:./drift-common/common-ts": version "1.0.0" dependencies: - "@drift-labs/sdk" "file:../../Library/Caches/Yarn/v6/npm-@drift-common-1.0.0-79940387-1595-4bdc-b73c-2bd48440a03a-1729802406625/node_modules/@drift/protocol/sdk" + "@drift-labs/sdk" "file:../../Library/Caches/Yarn/v6/npm-@drift-common-1.0.0-2478b7b0-73c2-48c2-8cdc-077c34db7294-1733168269147/node_modules/@drift/protocol/sdk" "@jest/globals" "^29.3.1" "@slack/web-api" "^6.4.0" "@solana/spl-token" "^0.3.8" @@ -2140,6 +2143,13 @@ js-yaml "^4.1.0" protobufjs "^7.2.6" +"@triton-one/yellowstone-grpc@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@triton-one/yellowstone-grpc/-/yellowstone-grpc-0.6.0.tgz#9e6376cec8a42284c23dc195df2c3423c87c4f27" + integrity sha512-rgdZM2N3U9/d/QKOI5PP+9rSHUl2oSI5Uwzvuss8y/mtTaHFjbOMpXpQXviIeDkusOa+mef4wLYrbjEZCwTXiw== + dependencies: + "@grpc/grpc-js" "^1.8.0" + "@triton-one/yellowstone-grpc@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@triton-one/yellowstone-grpc/-/yellowstone-grpc-0.3.0.tgz#2318b98b9ee80871e4ee8f9259f3d8673bf2c576" @@ -6307,7 +6317,7 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tweetnacl@^1.0.3: +tweetnacl@1.0.3, tweetnacl@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==