Merge branch 'master' into mainnet-beta

This commit is contained in:
wphan
2023-06-15 12:33:35 -07:00
8 changed files with 1077 additions and 734 deletions

View File

@@ -1,4 +1,3 @@
ANCHOR_PRIVATE_KEY=246,79,83,235,227,63,148,45,236,118,164,3,0,99,197,152,7,161,4,247,132,15,56,14,71,41,175,39,108,68,32,37,233,229,35,89,133,166,36,228,162,196,142,255,237,118,168,210,61,163,132,32,11,89,22,89,116,119,126,116,203,65,29,77
ENDPOINT=https://api.devnet.solana.com ENDPOINT=https://api.devnet.solana.com
ENV=devnet ENV=devnet
PORT=6969 PORT=6969

8
.prettierrc.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 80,
tabWidth: 2,
useTabs: true,
};

16
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Debug",
"port": 2230,
"timeout": 3000,
"restart": true
}
]
}

View File

@@ -5,7 +5,7 @@
"main": "lib/index.js", "main": "lib/index.js",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@drift-labs/sdk": "2.31.0-beta.1", "@drift-labs/sdk": "2.31.0-beta.7",
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
"@opentelemetry/auto-instrumentations-node": "^0.31.1", "@opentelemetry/auto-instrumentations-node": "^0.31.1",
"@opentelemetry/exporter-prometheus": "^0.31.0", "@opentelemetry/exporter-prometheus": "^0.31.0",
@@ -42,6 +42,7 @@
"start": "node lib/index.js", "start": "node lib/index.js",
"dev": "ts-node src/index.ts", "dev": "ts-node src/index.ts",
"dev:inspect": "yarn build && node --inspect ./lib/index.js", "dev:inspect": "yarn build && node --inspect ./lib/index.js",
"dev:debug": "yarn build && node --inspect-brk --inspect=2230 ./lib/index.js",
"example": "ts-node example/client.ts", "example": "ts-node example/client.ts",
"exampleWithSlot": "ts-node example/clientWithSlot.ts", "exampleWithSlot": "ts-node example/clientWithSlot.ts",
"prettify": "prettier --check './src/**/*.ts'", "prettify": "prettier --check './src/**/*.ts'",

View File

@@ -1,13 +1,13 @@
import { program, Option } from "commander"; import { program } from 'commander';
import responseTime = require("response-time"); import responseTime = require('response-time');
import express from "express"; import express from 'express';
import rateLimit from "express-rate-limit"; import rateLimit from 'express-rate-limit';
import compression from "compression"; import compression from 'compression';
import morgan from "morgan"; import morgan from 'morgan';
import cors from "cors"; import cors from 'cors';
import { Connection, Commitment, PublicKey } from "@solana/web3.js"; import { Connection, Commitment, PublicKey, Keypair } from '@solana/web3.js';
import { import {
getVariant, getVariant,
@@ -24,30 +24,37 @@ import {
PerpMarkets, PerpMarkets,
DLOBSubscriber, DLOBSubscriber,
MarketType, MarketType,
SpotMarketConfig,
PhoenixSubscriber,
SerumSubscriber,
DLOBNode,
isVariant, isVariant,
} from "@drift-labs/sdk"; BN,
groupL2,
L2OrderBook,
Wallet,
} from '@drift-labs/sdk';
import { Mutex } from "async-mutex"; import { Mutex } from 'async-mutex';
import { getWallet } from "./utils"; import { logger, setLogLevel } from './logger';
import { logger, setLogLevel } from "./logger";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus"; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { import {
ExplicitBucketHistogramAggregation, ExplicitBucketHistogramAggregation,
InstrumentType, InstrumentType,
MeterProvider, MeterProvider,
View, View,
} from "@opentelemetry/sdk-metrics-base"; } from '@opentelemetry/sdk-metrics-base';
import { ObservableResult } from "@opentelemetry/api"; import { ObservableResult } from '@opentelemetry/api';
require("dotenv").config(); require('dotenv').config();
const driftEnv = (process.env.ENV || "devnet") as DriftEnv; const driftEnv = (process.env.ENV || 'devnet') as DriftEnv;
const commitHash = process.env.COMMIT; const commitHash = process.env.COMMIT;
//@ts-ignore //@ts-ignore
const sdkConfig = initialize({ env: process.env.ENV }); const sdkConfig = initialize({ env: process.env.ENV });
const stateCommitment: Commitment = "confirmed"; const stateCommitment: Commitment = 'confirmed';
const serverPort = process.env.PORT || 6969; const serverPort = process.env.PORT || 6969;
const bulkAccountLoaderPollingInterval = process.env const bulkAccountLoaderPollingInterval = process.env
@@ -68,13 +75,13 @@ const logHttp = morgan(logFormat, {
function errorHandler(err, _req, res, _next) { function errorHandler(err, _req, res, _next) {
logger.error(err.stack); logger.error(err.stack);
res.status(500).send("Internal error"); res.status(500).send('Internal error');
} }
const app = express(); const app = express();
app.use(cors({ origin: "*" })); app.use(cors({ origin: '*' }));
app.use(compression()); app.use(compression());
app.set("trust proxy", 1); app.set('trust proxy', 1);
app.use(logHttp); app.use(logHttp);
app.use( app.use(
@@ -88,29 +95,17 @@ app.use(
// strip off /dlob, if the request comes from exchange history server LB // strip off /dlob, if the request comes from exchange history server LB
app.use((req, _res, next) => { app.use((req, _res, next) => {
if (req.url.startsWith("/dlob")) { if (req.url.startsWith('/dlob')) {
req.url = req.url.replace("/dlob", ""); req.url = req.url.replace('/dlob', '');
if (req.url === "") { if (req.url === '') {
req.url = "/"; req.url = '/';
} }
} }
next(); next();
}); });
program
.option("-d, --dry-run", "Dry run, do not send transactions on chain")
.option("--test-liveness", "Purposefully fail liveness test after 1 minute")
.addOption(
new Option(
"-p, --private-key <string>",
"private key, supports path to id.json, or list of comma separate numbers"
).env("ANCHOR_PRIVATE_KEY")
)
.option("--debug", "Enable debug logging")
.parse();
const opts = program.opts(); const opts = program.opts();
setLogLevel(opts.debug ? "debug" : "info"); setLogLevel(opts.debug ? 'debug' : 'info');
const endpoint = process.env.ENDPOINT; const endpoint = process.env.ENDPOINT;
const wsEndpoint = process.env.WS_ENDPOINT; const wsEndpoint = process.env.WS_ENDPOINT;
@@ -141,9 +136,9 @@ const createHistogramBuckets = (
}; };
enum METRIC_TYPES { enum METRIC_TYPES {
runtime_specs = "runtime_specs", runtime_specs = 'runtime_specs',
endpoint_response_times_histogram = "endpoint_response_times_histogram", endpoint_response_times_histogram = 'endpoint_response_times_histogram',
health_status = "health_status", health_status = 'health_status',
} }
export enum HEALTH_STATUS { export enum HEALTH_STATUS {
@@ -167,7 +162,7 @@ const exporter = new PrometheusExporter(
); );
} }
); );
const meterName = "dlob-meter"; const meterName = 'dlob-meter';
const meterProvider = new MeterProvider({ const meterProvider = new MeterProvider({
views: [ views: [
new View({ new View({
@@ -184,7 +179,7 @@ const meter = meterProvider.getMeter(meterName);
const runtimeSpecsGauge = meter.createObservableGauge( const runtimeSpecsGauge = meter.createObservableGauge(
METRIC_TYPES.runtime_specs, METRIC_TYPES.runtime_specs,
{ {
description: "Runtime sepcification of this program", description: 'Runtime sepcification of this program',
} }
); );
const bootTimeMs = Date.now(); const bootTimeMs = Date.now();
@@ -201,7 +196,7 @@ let healthStatus: HEALTH_STATUS = HEALTH_STATUS.Ok;
const healthStatusGauge = meter.createObservableGauge( const healthStatusGauge = meter.createObservableGauge(
METRIC_TYPES.health_status, METRIC_TYPES.health_status,
{ {
description: "Health status of this program", description: 'Health status of this program',
} }
); );
healthStatusGauge.addCallback((obs: ObservableResult) => { healthStatusGauge.addCallback((obs: ObservableResult) => {
@@ -211,13 +206,90 @@ healthStatusGauge.addCallback((obs: ObservableResult) => {
const endpointResponseTimeHistogram = meter.createHistogram( const endpointResponseTimeHistogram = meter.createHistogram(
METRIC_TYPES.endpoint_response_times_histogram, METRIC_TYPES.endpoint_response_times_histogram,
{ {
description: "Duration of endpoint responses", description: 'Duration of endpoint responses',
unit: "ms", unit: 'ms',
} }
); );
const getPhoenixSubscriber = (
driftClient: DriftClient,
marketConfig: SpotMarketConfig,
accountLoader: BulkAccountLoader
) => {
return new PhoenixSubscriber({
connection: driftClient.connection,
programId: new PublicKey(sdkConfig.PHOENIX),
marketAddress: marketConfig.phoenixMarket,
accountSubscription: {
type: 'polling',
accountLoader,
},
});
};
const getSerumSubscriber = (
driftClient: DriftClient,
marketConfig: SpotMarketConfig,
accountLoader: BulkAccountLoader
) => {
return new SerumSubscriber({
connection: driftClient.connection,
programId: new PublicKey(sdkConfig.SERUM_V3),
marketAddress: marketConfig.serumMarket,
accountSubscription: {
type: 'polling',
accountLoader,
},
});
};
type SubscriberLookup = {
[marketIndex: number]: {
phoenix?: PhoenixSubscriber;
serum?: SerumSubscriber;
};
};
let MARKET_SUBSCRIBERS: SubscriberLookup = {};
const initializeAllMarketSubscribers = async (
driftClient: DriftClient,
bulkAccountLoader: BulkAccountLoader
) => {
const markets: SubscriberLookup = {};
for (const market of sdkConfig.SPOT_MARKETS) {
markets[market.marketIndex] = {
phoenix: undefined,
serum: undefined,
};
if (market.phoenixMarket) {
const phoenixSubscriber = getPhoenixSubscriber(
driftClient,
market,
bulkAccountLoader
);
await phoenixSubscriber.subscribe();
markets[market.marketIndex].phoenix = phoenixSubscriber;
}
if (market.serumMarket) {
const serumSubscriber = getSerumSubscriber(
driftClient,
market,
bulkAccountLoader
);
await serumSubscriber.subscribe();
markets[market.marketIndex].serum = serumSubscriber;
}
}
return markets;
};
const main = async () => { const main = async () => {
const wallet = getWallet(); const wallet = new Wallet(new Keypair());
const clearingHousePublicKey = new PublicKey(sdkConfig.DRIFT_PROGRAM_ID); const clearingHousePublicKey = new PublicKey(sdkConfig.DRIFT_PROGRAM_ID);
const connection = new Connection(endpoint, { const connection = new Connection(endpoint, {
@@ -237,13 +309,8 @@ const main = async () => {
connection, connection,
wallet, wallet,
programID: clearingHousePublicKey, programID: clearingHousePublicKey,
perpMarketIndexes: PerpMarkets[driftEnv].map((mkt) => mkt.marketIndex),
spotMarketIndexes: SpotMarkets[driftEnv].map((mkt) => mkt.marketIndex),
oracleInfos: PerpMarkets[driftEnv].map((mkt) => {
return { publicKey: mkt.oracle, source: mkt.oracleSource };
}),
accountSubscription: { accountSubscription: {
type: "polling", type: 'polling',
accountLoader: bulkAccountLoader, accountLoader: bulkAccountLoader,
}, },
env: driftEnv, env: driftEnv,
@@ -266,29 +333,18 @@ const main = async () => {
logger.info(` . SOL balance: ${lamportsBalance / 10 ** 9}`); logger.info(` . SOL balance: ${lamportsBalance / 10 ** 9}`);
await driftClient.subscribe(); await driftClient.subscribe();
driftClient.eventEmitter.on("error", (e) => { driftClient.eventEmitter.on('error', (e) => {
logger.info("clearing house error"); logger.info('clearing house error');
logger.error(e); logger.error(e);
}); });
await slotSubscriber.subscribe(); await slotSubscriber.subscribe();
slotSubscriber.eventEmitter.on("newSlot", async (slot: number) => { slotSubscriber.eventEmitter.on('newSlot', async (slot: number) => {
await lastSlotReceivedMutex.runExclusive(async () => { await lastSlotReceivedMutex.runExclusive(async () => {
lastSlotReceived = slot; lastSlotReceived = slot;
}); });
}); });
if (!(await driftClient.getUser().exists())) {
logger.error(`User for ${wallet.publicKey} does not exist`);
if (opts.initUser) {
logger.info(`Creating User for ${wallet.publicKey}`);
const [txSig] = await driftClient.initializeUserAccount();
logger.info(`Initialized user account in transaction: ${txSig}`);
} else {
throw new Error("Run with '--init-user' flag to initialize a User");
}
}
const userMap = new UserMap( const userMap = new UserMap(
driftClient, driftClient,
driftClient.userAccountSubscriptionConfig, driftClient.userAccountSubscriptionConfig,
@@ -313,16 +369,21 @@ const main = async () => {
}); });
}); });
MARKET_SUBSCRIBERS = await initializeAllMarketSubscribers(
driftClient,
bulkAccountLoader
);
// start http server listening to /health endpoint using http package // start http server listening to /health endpoint using http package
app.get("/health", handleResponseTime, async (req, res, next) => { app.get('/health', handleResponseTime, async (req, res, next) => {
try { try {
if (req.url === "/health") { if (req.url === '/health') {
if (opts.testLiveness) { if (opts.testLiveness) {
if (Date.now() > startupTime + 60 * 1000) { if (Date.now() > startupTime + 60 * 1000) {
healthStatus = HEALTH_STATUS.LivenessTesting; healthStatus = HEALTH_STATUS.LivenessTesting;
res.writeHead(500); res.writeHead(500);
res.end("Testing liveness test fail"); res.end('Testing liveness test fail');
return; return;
} }
} }
@@ -382,17 +443,17 @@ const main = async () => {
// liveness check passed // liveness check passed
healthStatus = HEALTH_STATUS.Ok; healthStatus = HEALTH_STATUS.Ok;
res.writeHead(200); res.writeHead(200);
res.end("OK"); res.end('OK');
} else { } else {
res.writeHead(404); res.writeHead(404);
res.end("Not found"); res.end('Not found');
} }
} catch (e) { } catch (e) {
next(e); next(e);
} }
}); });
app.get("/orders/json/raw", handleResponseTime, async (_req, res, next) => { app.get('/orders/json/raw', handleResponseTime, async (_req, res, next) => {
try { try {
// object with userAccount key and orders object serialized // object with userAccount key and orders object serialized
const orders: Array<any> = []; const orders: Array<any> = [];
@@ -413,7 +474,7 @@ const main = async () => {
const userAccount = user.getUserAccount(); const userAccount = user.getUserAccount();
for (const order of userAccount.orders) { for (const order of userAccount.orders) {
if (getVariant(order.status) === "init") { if (isVariant(order.status, 'init')) {
continue; continue;
} }
@@ -438,7 +499,7 @@ const main = async () => {
} }
}); });
app.get("/orders/json", handleResponseTime, async (_req, res, next) => { app.get('/orders/json', handleResponseTime, async (_req, res, next) => {
try { try {
// object with userAccount key and orders object serialized // object with userAccount key and orders object serialized
const slot = bulkAccountLoader.mostRecentSlot; const slot = bulkAccountLoader.mostRecentSlot;
@@ -457,10 +518,10 @@ const main = async () => {
oracle.hasSufficientNumberOfDataPoints, oracle.hasSufficientNumberOfDataPoints,
}; };
if (oracle.twap) { if (oracle.twap) {
oracleHuman["twap"] = oracle.twap.toString(); oracleHuman['twap'] = oracle.twap.toString();
} }
if (oracle.twapConfidence) { if (oracle.twapConfidence) {
oracleHuman["twapConfidence"] = oracle.twapConfidence.toString(); oracleHuman['twapConfidence'] = oracle.twapConfidence.toString();
} }
oracles.push(oracleHuman); oracles.push(oracleHuman);
} }
@@ -468,7 +529,7 @@ const main = async () => {
const userAccount = user.getUserAccount(); const userAccount = user.getUserAccount();
for (const order of userAccount.orders) { for (const order of userAccount.orders) {
if (getVariant(order.status) === "init") { if (isVariant(order.status, 'init')) {
continue; continue;
} }
@@ -500,7 +561,7 @@ const main = async () => {
maxTs: order.maxTs.toString(), maxTs: order.maxTs.toString(),
}; };
if (order.quoteAssetAmount) { if (order.quoteAssetAmount) {
orderHuman["quoteAssetAmount"] = order.quoteAssetAmount.toString(); orderHuman['quoteAssetAmount'] = order.quoteAssetAmount.toString();
} }
orders.push({ orders.push({
@@ -524,7 +585,7 @@ const main = async () => {
} }
}); });
app.get("/orders/idl", handleResponseTime, async (_req, res, next) => { app.get('/orders/idl', handleResponseTime, async (_req, res, next) => {
try { try {
const dlobOrders: DLOBOrders = []; const dlobOrders: DLOBOrders = [];
@@ -532,7 +593,7 @@ const main = async () => {
const userAccount = user.getUserAccount(); const userAccount = user.getUserAccount();
for (const order of userAccount.orders) { for (const order of userAccount.orders) {
if (getVariant(order.status) === "init") { if (isVariant(order.status, 'init')) {
continue; continue;
} }
@@ -550,7 +611,7 @@ const main = async () => {
} }
}); });
app.get("/orders/idlWithSlot", handleResponseTime, async (req, res, next) => { app.get('/orders/idlWithSlot', handleResponseTime, async (req, res, next) => {
try { try {
const { marketName, marketIndex, marketType } = req.query; const { marketName, marketIndex, marketType } = req.query;
const { normedMarketType, normedMarketIndex, error } = validateDlobQuery( const { normedMarketType, normedMarketIndex, error } = validateDlobQuery(
@@ -580,7 +641,7 @@ const main = async () => {
const userAccount = user.getUserAccount(); const userAccount = user.getUserAccount();
for (const order of userAccount.orders) { for (const order of userAccount.orders) {
if (getVariant(order.status) === "init") { if (isVariant(order.status, 'init')) {
continue; continue;
} }
@@ -603,7 +664,7 @@ const main = async () => {
res.end( res.end(
JSON.stringify({ JSON.stringify({
slot: bulkAccountLoader.mostRecentSlot, slot: bulkAccountLoader.mostRecentSlot,
data: dlobCoder.encode(dlobOrders).toString("base64"), data: dlobCoder.encode(dlobOrders).toString('base64'),
}) })
); );
} catch (err) { } catch (err) {
@@ -627,13 +688,13 @@ const main = async () => {
if (marketIndex === undefined || marketType === undefined) { if (marketIndex === undefined || marketType === undefined) {
return { return {
error: error:
"Bad Request: (marketName) or (marketIndex and marketType) must be supplied", 'Bad Request: (marketName) or (marketIndex and marketType) must be supplied',
}; };
} }
// validate marketType // validate marketType
switch ((marketType as string).toLowerCase()) { switch ((marketType as string).toLowerCase()) {
case "spot": { case 'spot': {
normedMarketType = MarketType.SPOT; normedMarketType = MarketType.SPOT;
normedMarketIndex = parseInt(marketIndex as string); normedMarketIndex = parseInt(marketIndex as string);
const spotMarketIndicies = SpotMarkets[driftEnv].map( const spotMarketIndicies = SpotMarkets[driftEnv].map(
@@ -641,12 +702,12 @@ const main = async () => {
); );
if (!spotMarketIndicies.includes(normedMarketIndex)) { if (!spotMarketIndicies.includes(normedMarketIndex)) {
return { return {
error: "Bad Request: invalid marketIndex", error: 'Bad Request: invalid marketIndex',
}; };
} }
break; break;
} }
case "perp": { case 'perp': {
normedMarketType = MarketType.PERP; normedMarketType = MarketType.PERP;
normedMarketIndex = parseInt(marketIndex as string); normedMarketIndex = parseInt(marketIndex as string);
const perpMarketIndicies = PerpMarkets[driftEnv].map( const perpMarketIndicies = PerpMarkets[driftEnv].map(
@@ -654,7 +715,7 @@ const main = async () => {
); );
if (!perpMarketIndicies.includes(normedMarketIndex)) { if (!perpMarketIndicies.includes(normedMarketIndex)) {
return { return {
error: "Bad Request: invalid marketIndex", error: 'Bad Request: invalid marketIndex',
}; };
} }
break; break;
@@ -671,7 +732,7 @@ const main = async () => {
driftClient.getMarketIndexAndType(normedMarketName); driftClient.getMarketIndexAndType(normedMarketName);
if (!derivedMarketInfo) { if (!derivedMarketInfo) {
return { return {
error: "Bad Request: unrecognized marketName", error: 'Bad Request: unrecognized marketName',
}; };
} }
normedMarketType = derivedMarketInfo.marketType; normedMarketType = derivedMarketInfo.marketType;
@@ -684,10 +745,15 @@ const main = async () => {
}; };
}; };
app.get("/l2", handleResponseTime, async (req, res, next) => { app.get('/topMakers', handleResponseTime, async (req, res, next) => {
try { try {
const { marketName, marketIndex, marketType, depth, includeVamm } = const {
req.query; marketName,
marketIndex,
marketType,
side, // bid or ask
limit, // number of unique makers to return, if undefined will return all
} = req.query;
const { normedMarketType, normedMarketIndex, error } = validateDlobQuery( const { normedMarketType, normedMarketIndex, error } = validateDlobQuery(
marketType as string, marketType as string,
@@ -699,19 +765,81 @@ const main = async () => {
return; return;
} }
const l2 = dlobSubscriber.getL2({ if (side !== 'bid' && side !== 'ask') {
marketIndex: normedMarketIndex, res.status(400).send('Bad Request: side must be either bid or ask');
marketType: normedMarketType, return;
depth: depth ? parseInt(depth as string) : 10, }
includeVamm: includeVamm const normedSide = (side as string).toLowerCase();
? (includeVamm as string).toLowerCase() === "true" const oracle = driftClient.getOracleDataForPerpMarket(normedMarketIndex);
: false,
let normedLimit = undefined;
if (limit) {
if (isNaN(parseInt(limit as string))) {
res
.status(400)
.send('Bad Request: limit must be a number if supplied');
return;
}
normedLimit = parseInt(limit as string);
}
const topMakers: Set<string> = new Set();
let foundMakers = 0;
const findMakers = (sideGenerator: Generator<DLOBNode>) => {
for (const side of sideGenerator) {
if (limit && foundMakers >= normedLimit) {
break;
}
if (side.userAccount) {
const maker = side.userAccount.toBase58();
if (topMakers.has(maker)) {
continue;
} else {
topMakers.add(side.userAccount.toBase58());
foundMakers++;
}
} else {
continue;
}
}
};
if (normedSide === 'bid') {
findMakers(
dlobSubscriber
.getDLOB()
.getRestingLimitBids(
normedMarketIndex,
slotSubscriber.getSlot(),
normedMarketType,
oracle
)
);
} else {
findMakers(
dlobSubscriber
.getDLOB()
.getRestingLimitAsks(
normedMarketIndex,
slotSubscriber.getSlot(),
normedMarketType,
oracle
)
);
}
res.writeHead(200);
res.end(JSON.stringify([...topMakers]));
} catch (err) {
next(err);
}
}); });
const l2WithBNToStrings = (l2: L2OrderBook): any => {
for (const key of Object.keys(l2)) { for (const key of Object.keys(l2)) {
for (const idx in l2[key]) { for (const idx in l2[key]) {
const level = l2[key][idx]; const level = l2[key][idx];
const sources = level["sources"]; const sources = level['sources'];
for (const sourceKey of Object.keys(sources)) { for (const sourceKey of Object.keys(sources)) {
sources[sourceKey] = sources[sourceKey].toString(); sources[sourceKey] = sources[sourceKey].toString();
} }
@@ -722,15 +850,242 @@ const main = async () => {
}; };
} }
} }
return l2;
};
app.get('/l2', handleResponseTime, async (req, res, next) => {
try {
const {
marketName,
marketIndex,
marketType,
depth,
includeVamm,
includePhoenix,
includeSerum,
grouping, // undefined or PRICE_PRECISION
} = req.query;
const { normedMarketType, normedMarketIndex, error } = validateDlobQuery(
marketType as string,
marketIndex as string,
marketName as string
);
if (error) {
res.status(400).send(error);
return;
}
const isSpot = isVariant(normedMarketType, 'spot');
let adjustedDepth = depth ?? '10';
if (grouping !== undefined) {
// If grouping is also supplied, we want the entire book depth.
// we will apply depth after grouping
adjustedDepth = '-1';
}
const l2 = dlobSubscriber.getL2({
marketIndex: normedMarketIndex,
marketType: normedMarketType,
depth: parseInt(adjustedDepth as string),
includeVamm: `${includeVamm}`.toLowerCase() === 'true',
fallbackL2Generators: isSpot
? [
`${includePhoenix}`.toLowerCase() === 'true' &&
MARKET_SUBSCRIBERS[normedMarketIndex].phoenix,
`${includeSerum}`.toLowerCase() === 'true' &&
MARKET_SUBSCRIBERS[normedMarketIndex].serum,
].filter((a) => !!a)
: [],
});
if (grouping) {
const finalDepth = depth ? parseInt(depth as string) : 10;
if (isNaN(parseInt(grouping as string))) {
res
.status(400)
.send('Bad Request: grouping must be a number if supplied');
return;
}
const groupingBN = new BN(parseInt(grouping as string));
res.writeHead(200); res.writeHead(200);
res.end(JSON.stringify(l2)); res.end(
JSON.stringify(l2WithBNToStrings(groupL2(l2, groupingBN, finalDepth)))
);
} else {
// make the BNs into strings
res.writeHead(200);
res.end(JSON.stringify(l2WithBNToStrings(l2)));
}
} catch (err) { } catch (err) {
next(err); next(err);
} }
}); });
app.get("/l3", handleResponseTime, async (req, res, next) => { /**
* Takes in a req.query like: `{
* marketName: 'SOL-PERP,BTC-PERP,ETH-PERP',
* marketType: undefined,
* marketIndices: undefined,
* ...
* }` and returns a normalized object like:
*
* `[
* {marketName: 'SOL-PERP', marketType: undefined, marketIndex: undefined,...},
* {marketName: 'BTC-PERP', marketType: undefined, marketIndex: undefined,...},
* {marketName: 'ETH-PERP', marketType: undefined, marketIndex: undefined,...}
* ]`
*
* @param rawParams req.query object
* @returns normalized query params for batch requests, or undefined if there is a mismatched length
*/
const normalizeBatchQueryParams = (rawParams: {
[key: string]: string | undefined;
}): Array<{ [key: string]: string | undefined }> => {
const normedParams: Array<{ [key: string]: string | undefined }> = [];
const parsedParams = {};
// parse the query string into arrays
for (const key of Object.keys(rawParams)) {
const rawParam = rawParams[key];
if (rawParam === undefined) {
parsedParams[key] = [];
} else {
parsedParams[key] = rawParam.split(',') || [rawParam];
}
}
// of all parsedParams, find the max length
const maxLength = Math.max(
...Object.values(parsedParams).map(
(param: Array<unknown>) => param.length
)
);
// all params have to be either 0 length, or maxLength to be valid
const values = Object.values(parsedParams);
const validParams = values.every(
(value: Array<unknown>) =>
value.length === 0 || value.length === maxLength
);
if (!validParams) {
return undefined;
}
// merge all params into an array of objects
// normalize all params to the same length, filling in undefineds
for (let i = 0; i < maxLength; i++) {
const newParam = {};
for (const key of Object.keys(parsedParams)) {
const parsedParam = parsedParams[key];
newParam[key] =
parsedParam.length === maxLength ? parsedParam[i] : undefined;
}
normedParams.push(newParam);
}
return normedParams;
};
app.get('/batchL2', handleResponseTime, async (req, res, next) => {
try {
const {
marketName,
marketIndex,
marketType,
depth,
includeVamm,
includePhoenix,
includeSerum,
grouping, // undefined or PRICE_PRECISION
} = req.query;
const normedParams = normalizeBatchQueryParams({
marketName: marketName as string | undefined,
marketIndex: marketIndex as string | undefined,
marketType: marketType as string | undefined,
depth: depth as string | undefined,
includeVamm: includeVamm as string | undefined,
includePhoenix: includePhoenix as string | undefined,
includeSerum: includeSerum as string | undefined,
grouping: grouping as string | undefined,
});
if (normedParams === undefined) {
res
.status(400)
.send(
'Bad Request: all params for batch request must be the same length'
);
return;
}
const l2s = normedParams.map((normedParam) => {
const { normedMarketType, normedMarketIndex, error } =
validateDlobQuery(
normedParam['marketType'] as string,
normedParam['marketIndex'] as string,
normedParam['marketName'] as string
);
if (error) {
res.status(400).send(`Bad Request: ${error}`);
return;
}
const isSpot = isVariant(normedMarketType, 'spot');
let adjustedDepth = normedParam['depth'] ?? '10';
if (normedParam['grouping'] !== undefined) {
// If grouping is also supplied, we want the entire book depth.
// we will apply depth after grouping
adjustedDepth = '-1';
}
const l2 = dlobSubscriber.getL2({
marketIndex: normedMarketIndex,
marketType: normedMarketType,
depth: parseInt(adjustedDepth as string),
includeVamm: `${normedParam['includeVamm']}`.toLowerCase() === 'true',
fallbackL2Generators: isSpot
? [
`${normedParam['includePhoenix']}`.toLowerCase() === 'true' &&
MARKET_SUBSCRIBERS[normedMarketIndex].phoenix,
`${normedParam['includeSerum']}`.toLowerCase() === 'true' &&
MARKET_SUBSCRIBERS[normedMarketIndex].serum,
].filter((a) => !!a)
: [],
});
if (normedParam['grouping']) {
const finalDepth = normedParam['depth']
? parseInt(normedParam['depth'] as string)
: 10;
if (isNaN(parseInt(normedParam['grouping'] as string))) {
res
.status(400)
.send('Bad Request: grouping must be a number if supplied');
return;
}
const groupingBN = new BN(
parseInt(normedParam['grouping'] as string)
);
return l2WithBNToStrings(groupL2(l2, groupingBN, finalDepth));
} else {
// make the BNs into strings
return l2WithBNToStrings(l2);
}
});
res.writeHead(200);
res.end(JSON.stringify({ l2s }));
} catch (err) {
next(err);
}
});
app.get('/l3', handleResponseTime, async (req, res, next) => {
try { try {
const { marketName, marketIndex, marketType } = req.query; const { marketName, marketIndex, marketType } = req.query;

View File

@@ -1,4 +1,4 @@
import { createLogger, transports, format } from "winston"; import { createLogger, transports, format } from 'winston';
export const logger = createLogger({ export const logger = createLogger({
transports: [new transports.Console()], transports: [new transports.Console()],

View File

@@ -1,36 +0,0 @@
import fs from "fs";
import { Keypair } from "@solana/web3.js";
import { bs58 } from "@project-serum/anchor/dist/cjs/utils/bytes";
import { Wallet } from "@drift-labs/sdk";
import { logger } from "./logger";
export function getWallet(): Wallet {
const privateKey = process.env.ANCHOR_PRIVATE_KEY;
if (!privateKey) {
throw new Error(
"Must set environment variable ANCHOR_PRIVATE_KEY with the path to a id.json or a list of commma separated numbers"
);
}
// try to load privateKey as a filepath
let loadedKey: Uint8Array;
if (fs.existsSync(privateKey)) {
logger.info(`loading private key from ${privateKey}`);
loadedKey = new Uint8Array(
JSON.parse(fs.readFileSync(privateKey).toString())
);
} else {
if (privateKey.includes(",")) {
logger.info(`Trying to load private key as comma separated numbers`);
loadedKey = Uint8Array.from(
privateKey.split(",").map((val) => Number(val))
);
} else {
logger.info(`Trying to load private key as base58 string`);
loadedKey = new Uint8Array(bs58.decode(privateKey));
}
}
const keypair = Keypair.fromSecretKey(Uint8Array.from(loadedKey));
return new Wallet(keypair);
}

View File

@@ -80,10 +80,10 @@
enabled "2.0.x" enabled "2.0.x"
kuler "^2.0.0" kuler "^2.0.0"
"@drift-labs/sdk@2.31.0-beta.1": "@drift-labs/sdk@2.31.0-beta.7":
version "2.31.0-beta.1" version "2.31.0-beta.7"
resolved "https://registry.yarnpkg.com/@drift-labs/sdk/-/sdk-2.31.0-beta.1.tgz#60e14806430fdcd427e9949ab511ab659c8a83fc" resolved "https://registry.yarnpkg.com/@drift-labs/sdk/-/sdk-2.31.0-beta.7.tgz#d7337d80d7ed9bd8a660cfd5280d660987e42f37"
integrity sha512-GQENFRU+qNIeFlC0k/JaaluTtgAQ4t6a9EHWfa4sIC6T+2KhBu2e6zZ0QiCSDYFm5cJleqE9z3EHEW3T2o5Exg== integrity sha512-+SpE4gv8BXvr/y/Gdjk7VS96Qg7u8SCMM8cB+WyI8KrJJsz569ORSEUu4FE2PVcu5HeypzUosiphLfDmZuXDIA==
dependencies: dependencies:
"@coral-xyz/anchor" "0.26.0" "@coral-xyz/anchor" "0.26.0"
"@ellipsis-labs/phoenix-sdk" "^1.4.2" "@ellipsis-labs/phoenix-sdk" "^1.4.2"