improved dlob ws rotation (#291)
* remove grpc impl, use sdk grpc OrderSubscriber * add log if both USE_WEBSOCKET and USE_GRPC are provided * Nour/improved rotation (#290) * improved rotation * fix bugs * only parse slot if necessary * add metrics for ws source * add select by recent slot to utils * fix solana web3.js version --------- Co-authored-by: wphan <william@drift.trade> --------- Co-authored-by: wphan <william@drift.trade>
This commit is contained in:
Submodule drift-common updated: 915ab84fe0...0303c32d03
@@ -1,5 +1,6 @@
|
|||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
const ws = new WebSocket('wss://dlob.drift.trade/ws');
|
// const ws = new WebSocket('wss://dlob.drift.trade/ws');
|
||||||
|
const ws = new WebSocket('http://localhost:3000/ws');
|
||||||
import { sleep } from '../src/utils/utils';
|
import { sleep } from '../src/utils/utils';
|
||||||
|
|
||||||
ws.on('open', async () => {
|
ws.on('open', async () => {
|
||||||
@@ -12,22 +13,26 @@ ws.on('open', async () => {
|
|||||||
ws.send(JSON.stringify({ type: 'subscribe', marketType: 'spot', channel: 'orderbook', market: 'SOL' }));
|
ws.send(JSON.stringify({ type: 'subscribe', marketType: 'spot', channel: 'orderbook', market: 'SOL' }));
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'unsubscribe', marketType: 'perp', channel: 'orderbook', market: 'SOL-PERP' }));
|
// ws.send(JSON.stringify({ type: 'unsubscribe', marketType: 'perp', channel: 'orderbook', market: 'SOL-PERP' }));
|
||||||
console.log("####################");
|
console.log("####################");
|
||||||
|
|
||||||
|
|
||||||
// Subscribe to trades data
|
// Subscribe to trades data
|
||||||
ws.send(JSON.stringify({ type: 'subscribe', marketType: 'perp', channel: 'trades', market: 'SOL-PERP' }));
|
ws.send(JSON.stringify({ type: 'subscribe', marketType: 'perp', channel: 'trades', market: 'SOL-PERP' }));
|
||||||
ws.send(JSON.stringify({ type: 'subscribe', marketType: 'spot', channel: 'trades', market: 'SOL' }));
|
ws.send(JSON.stringify({ type: 'subscribe', marketType: 'spot', channel: 'trades', market: 'SOL' }));
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'unsubscribe', marketType: 'perp', channel: 'trades', market: 'SOL-PERP' }));
|
// ws.send(JSON.stringify({ type: 'unsubscribe', marketType: 'perp', channel: 'trades', market: 'SOL-PERP' }));
|
||||||
console.log("####################");
|
console.log("####################");
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('message', (data: WebSocket.Data) => {
|
ws.on('message', (data: WebSocket.Data) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(data.toString());
|
const message = JSON.parse(data.toString());
|
||||||
|
if (!message.channel) {
|
||||||
|
console.log(`Received data: ${JSON.stringify(message)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log(`Received data from channel: ${JSON.stringify(message.channel)}`);
|
console.log(`Received data from channel: ${JSON.stringify(message.channel)}`);
|
||||||
// book and trades data is in message.data
|
// book and trades data is in message.data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"@opentelemetry/sdk-node": "^0.31.0",
|
"@opentelemetry/sdk-node": "^0.31.0",
|
||||||
"@project-serum/anchor": "^0.19.1-beta.1",
|
"@project-serum/anchor": "^0.19.1-beta.1",
|
||||||
"@project-serum/serum": "^0.13.65",
|
"@project-serum/serum": "^0.13.65",
|
||||||
"@solana/web3.js": "^1.73.3",
|
"@solana/web3.js": "1.95.2",
|
||||||
"@triton-one/yellowstone-grpc": "^0.3.0",
|
"@triton-one/yellowstone-grpc": "^0.3.0",
|
||||||
"@types/redis": "^4.0.11",
|
"@types/redis": "^4.0.11",
|
||||||
"@types/ws": "^8.5.8",
|
"@types/ws": "^8.5.8",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from '@opentelemetry/sdk-metrics-base';
|
} from '@opentelemetry/sdk-metrics-base';
|
||||||
import { SlotSource } from '@drift-labs/sdk';
|
import { SlotSource } from '@drift-labs/sdk';
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates {count} buckets of size {increment} starting from {start}. Each bucket stores the count of values within its "size".
|
* Creates {count} buckets of size {increment} starting from {start}. Each bucket stores the count of values within its "size".
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DLOB, OrderSubscriber, UserAccount, UserMap } from '@drift-labs/sdk';
|
import { DLOB, OrderSubscriber, UserAccount, UserMap } from '@drift-labs/sdk';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { GeyserOrderSubscriber } from './grpc/OrderSubscriberGRPC';
|
|
||||||
|
|
||||||
export type DLOBProvider = {
|
export type DLOBProvider = {
|
||||||
subscribe(): Promise<void>;
|
subscribe(): Promise<void>;
|
||||||
@@ -91,45 +90,3 @@ export function getDLOBProviderFromOrderSubscriber(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDLOBProviderFromGrpcOrderSubscriber(
|
|
||||||
orderSubscriber: GeyserOrderSubscriber
|
|
||||||
): DLOBProvider {
|
|
||||||
return {
|
|
||||||
subscribe: async () => {
|
|
||||||
await orderSubscriber.subscribe();
|
|
||||||
},
|
|
||||||
getDLOB: async (slot: number) => {
|
|
||||||
return await orderSubscriber.getDLOB(slot);
|
|
||||||
},
|
|
||||||
getUniqueAuthorities: () => {
|
|
||||||
const authorities = new Set<string>();
|
|
||||||
for (const { userAccount } of orderSubscriber.usersAccounts.values()) {
|
|
||||||
authorities.add(userAccount.authority.toBase58());
|
|
||||||
}
|
|
||||||
const pubkeys = Array.from(authorities).map((a) => new PublicKey(a));
|
|
||||||
return pubkeys;
|
|
||||||
},
|
|
||||||
getUserAccounts: function* () {
|
|
||||||
for (const [
|
|
||||||
key,
|
|
||||||
{ userAccount },
|
|
||||||
] of orderSubscriber.usersAccounts.entries()) {
|
|
||||||
yield { userAccount: userAccount, publicKey: new PublicKey(key) };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getUserAccount: (publicKey) => {
|
|
||||||
return orderSubscriber.usersAccounts.get(publicKey.toString())
|
|
||||||
?.userAccount;
|
|
||||||
},
|
|
||||||
size(): number {
|
|
||||||
return orderSubscriber.usersAccounts.size;
|
|
||||||
},
|
|
||||||
fetch() {
|
|
||||||
return orderSubscriber.fetch();
|
|
||||||
},
|
|
||||||
getSlot: () => {
|
|
||||||
return orderSubscriber.getSlot();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
import { BorshAccountsCoder } from '@coral-xyz/anchor';
|
|
||||||
import { Commitment, PublicKey, RpcResponseAndContext } from '@solana/web3.js';
|
|
||||||
import { Buffer } from 'buffer';
|
|
||||||
import Client, {
|
|
||||||
CommitmentLevel,
|
|
||||||
SubscribeRequest,
|
|
||||||
SubscribeUpdate,
|
|
||||||
} from '@triton-one/yellowstone-grpc';
|
|
||||||
import {
|
|
||||||
BN,
|
|
||||||
DLOB,
|
|
||||||
DriftClient,
|
|
||||||
UserAccount,
|
|
||||||
getUserFilter,
|
|
||||||
getUserWithOrderFilter,
|
|
||||||
} from '@drift-labs/sdk';
|
|
||||||
import { ClientDuplexStream } from '@grpc/grpc-js';
|
|
||||||
|
|
||||||
type grpcDlobConfig = {
|
|
||||||
endpoint: string;
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class GeyserOrderSubscriber {
|
|
||||||
usersAccounts = new Map<string, { slot: number; userAccount: UserAccount }>();
|
|
||||||
commitment: Commitment;
|
|
||||||
driftClient: DriftClient;
|
|
||||||
config: grpcDlobConfig;
|
|
||||||
stream: ClientDuplexStream<SubscribeRequest, SubscribeUpdate>;
|
|
||||||
|
|
||||||
fetchPromise?: Promise<void>;
|
|
||||||
fetchPromiseResolver: () => void;
|
|
||||||
mostRecentSlot: number;
|
|
||||||
|
|
||||||
constructor(driftClient: DriftClient, config: grpcDlobConfig) {
|
|
||||||
this.driftClient = driftClient;
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async subscribe(): Promise<void> {
|
|
||||||
const client = new Client(this.config.endpoint, this.config.token);
|
|
||||||
this.stream = await client.subscribe();
|
|
||||||
const request: SubscribeRequest = {
|
|
||||||
slots: {},
|
|
||||||
accounts: {
|
|
||||||
drift: {
|
|
||||||
owner: [this.driftClient.program.programId.toBase58()],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
memcmp: {
|
|
||||||
offset: '0',
|
|
||||||
bytes: BorshAccountsCoder.accountDiscriminator('User'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memcmp: {
|
|
||||||
offset: '4350',
|
|
||||||
bytes: Uint8Array.from([1]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
account: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transactions: {},
|
|
||||||
blocks: {},
|
|
||||||
blocksMeta: {},
|
|
||||||
accountsDataSlice: [],
|
|
||||||
commitment: CommitmentLevel.PROCESSED,
|
|
||||||
entry: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.stream.on('data', (chunk: any) => {
|
|
||||||
if (!chunk.account) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const slot = Number(chunk.account.slot);
|
|
||||||
this.tryUpdateUserAccount(
|
|
||||||
new PublicKey(chunk.account.account.pubkey).toBase58(),
|
|
||||||
'grpc',
|
|
||||||
chunk.account.account.data,
|
|
||||||
slot
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
this.stream.write(request, (err) => {
|
|
||||||
if (err === null || err === undefined) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch((reason) => {
|
|
||||||
console.error(reason);
|
|
||||||
throw reason;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
if (this.fetchPromise) {
|
|
||||||
return this.fetchPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchPromise = new Promise((resolver) => {
|
|
||||||
this.fetchPromiseResolver = resolver;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rpcRequestArgs = [
|
|
||||||
this.driftClient.program.programId.toBase58(),
|
|
||||||
{
|
|
||||||
commitment: this.commitment,
|
|
||||||
filters: [getUserFilter(), getUserWithOrderFilter()],
|
|
||||||
encoding: 'base64',
|
|
||||||
withContext: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const rpcJSONResponse: any =
|
|
||||||
// @ts-ignore
|
|
||||||
await this.driftClient.connection._rpcRequest(
|
|
||||||
'getProgramAccounts',
|
|
||||||
rpcRequestArgs
|
|
||||||
);
|
|
||||||
|
|
||||||
const rpcResponseAndContext: RpcResponseAndContext<
|
|
||||||
Array<{
|
|
||||||
pubkey: PublicKey;
|
|
||||||
account: {
|
|
||||||
data: [string, string];
|
|
||||||
};
|
|
||||||
}>
|
|
||||||
> = rpcJSONResponse.result;
|
|
||||||
|
|
||||||
const slot: number = rpcResponseAndContext.context.slot;
|
|
||||||
|
|
||||||
const programAccountSet = new Set<string>();
|
|
||||||
for (const programAccount of rpcResponseAndContext.value) {
|
|
||||||
const key = programAccount.pubkey.toString();
|
|
||||||
programAccountSet.add(key);
|
|
||||||
this.tryUpdateUserAccount(
|
|
||||||
key,
|
|
||||||
'raw',
|
|
||||||
programAccount.account.data,
|
|
||||||
slot
|
|
||||||
);
|
|
||||||
// give event loop a chance to breathe
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of this.usersAccounts.keys()) {
|
|
||||||
if (!programAccountSet.has(key)) {
|
|
||||||
this.usersAccounts.delete(key);
|
|
||||||
}
|
|
||||||
// give event loop a chance to breathe
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
this.fetchPromiseResolver();
|
|
||||||
this.fetchPromise = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryUpdateUserAccount(
|
|
||||||
key: string,
|
|
||||||
dataType: 'raw' | 'grpc',
|
|
||||||
data: string[] | Buffer | UserAccount,
|
|
||||||
slot: number
|
|
||||||
): void {
|
|
||||||
if (!this.mostRecentSlot || slot > this.mostRecentSlot) {
|
|
||||||
this.mostRecentSlot = slot;
|
|
||||||
}
|
|
||||||
|
|
||||||
const slotAndUserAccount = this.usersAccounts.get(key);
|
|
||||||
if (!slotAndUserAccount || slotAndUserAccount.slot <= slot) {
|
|
||||||
// Polling leads to a lot of redundant decoding, so we only decode if data is from a fresh slot
|
|
||||||
let buffer: Buffer;
|
|
||||||
if (dataType === 'raw') {
|
|
||||||
buffer = Buffer.from(data[0], data[1]);
|
|
||||||
} else {
|
|
||||||
buffer = data as Buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLastActiveSlot = new BN(
|
|
||||||
buffer.subarray(4328, 4328 + 8),
|
|
||||||
undefined,
|
|
||||||
'le'
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
slotAndUserAccount &&
|
|
||||||
slotAndUserAccount.userAccount.lastActiveSlot.gt(newLastActiveSlot)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userAccount =
|
|
||||||
this.driftClient.program.account.user.coder.accounts.decodeUnchecked(
|
|
||||||
'User',
|
|
||||||
buffer
|
|
||||||
) as UserAccount;
|
|
||||||
|
|
||||||
if (userAccount.hasOpenOrder) {
|
|
||||||
this.usersAccounts.set(key, { slot, userAccount });
|
|
||||||
} else {
|
|
||||||
this.usersAccounts.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDLOB(slot: number): Promise<DLOB> {
|
|
||||||
const dlob = new DLOB();
|
|
||||||
for (const [key, { userAccount }] of this.usersAccounts.entries()) {
|
|
||||||
const userAccountPubkey = new PublicKey(key);
|
|
||||||
for (const order of userAccount.orders) {
|
|
||||||
dlob.insertOrder(order, userAccountPubkey.toBase58(), slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dlob;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSlot(): number {
|
|
||||||
return this.mostRecentSlot ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unsubscribe(): Promise<void> {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
this.stream.write(
|
|
||||||
{
|
|
||||||
slots: {},
|
|
||||||
accounts: {},
|
|
||||||
transactions: {},
|
|
||||||
blocks: {},
|
|
||||||
blocksMeta: {},
|
|
||||||
accountsDataSlice: [],
|
|
||||||
entry: {},
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
if (err === null || err === undefined) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}).catch((reason) => {
|
|
||||||
console.error(reason);
|
|
||||||
throw reason;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
362
src/index.ts
362
src/index.ts
@@ -17,7 +17,6 @@ import {
|
|||||||
initialize,
|
initialize,
|
||||||
isVariant,
|
isVariant,
|
||||||
OrderSubscriber,
|
OrderSubscriber,
|
||||||
MarketType,
|
|
||||||
PhoenixSubscriber,
|
PhoenixSubscriber,
|
||||||
BulkAccountLoader,
|
BulkAccountLoader,
|
||||||
isOperationPaused,
|
isOperationPaused,
|
||||||
@@ -49,6 +48,7 @@ import {
|
|||||||
getAccountFromId,
|
getAccountFromId,
|
||||||
getRawAccountFromId,
|
getRawAccountFromId,
|
||||||
getOpenbookSubscriber,
|
getOpenbookSubscriber,
|
||||||
|
selectMostRecentBySlot,
|
||||||
} from './utils/utils';
|
} from './utils/utils';
|
||||||
import FEATURE_FLAGS from './utils/featureFlags';
|
import FEATURE_FLAGS from './utils/featureFlags';
|
||||||
import { getDLOBProviderFromOrderSubscriber } from './dlobProvider';
|
import { getDLOBProviderFromOrderSubscriber } from './dlobProvider';
|
||||||
@@ -84,10 +84,8 @@ const serverPort = process.env.PORT || 6969;
|
|||||||
export const ORDERBOOK_UPDATE_INTERVAL = 1000;
|
export const ORDERBOOK_UPDATE_INTERVAL = 1000;
|
||||||
const WS_FALLBACK_FETCH_INTERVAL = ORDERBOOK_UPDATE_INTERVAL * 60;
|
const WS_FALLBACK_FETCH_INTERVAL = ORDERBOOK_UPDATE_INTERVAL * 60;
|
||||||
const SLOT_STALENESS_TOLERANCE =
|
const SLOT_STALENESS_TOLERANCE =
|
||||||
parseInt(process.env.SLOT_STALENESS_TOLERANCE) || 35;
|
parseInt(process.env.SLOT_STALENESS_TOLERANCE) || 1000;
|
||||||
const ROTATION_COOLDOWN = parseInt(process.env.ROTATION_COOLDOWN) || 5000;
|
|
||||||
const useWebsocket = process.env.USE_WEBSOCKET?.toLowerCase() === 'true';
|
const useWebsocket = process.env.USE_WEBSOCKET?.toLowerCase() === 'true';
|
||||||
const useRedis = process.env.USE_REDIS?.toLowerCase() === 'true';
|
|
||||||
|
|
||||||
const logFormat =
|
const logFormat =
|
||||||
':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :req[x-forwarded-for]';
|
':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :req[x-forwarded-for]';
|
||||||
@@ -299,107 +297,27 @@ const main = async (): Promise<void> => {
|
|||||||
|
|
||||||
// Handle redis client initialization and rotation maps
|
// Handle redis client initialization and rotation maps
|
||||||
const redisClients: Array<RedisClient> = [];
|
const redisClients: Array<RedisClient> = [];
|
||||||
const spotMarketRedisMap: Map<
|
logger.info('Connecting to redis');
|
||||||
number,
|
for (let i = 0; i < REDIS_CLIENTS.length; i++) {
|
||||||
{
|
redisClients.push(new RedisClient({ prefix: REDIS_CLIENTS[i] }));
|
||||||
client: RedisClient;
|
|
||||||
clientIndex: number;
|
|
||||||
lastRotationTime: number;
|
|
||||||
lock: boolean;
|
|
||||||
}
|
|
||||||
> = new Map();
|
|
||||||
const perpMarketRedisMap: Map<
|
|
||||||
number,
|
|
||||||
{
|
|
||||||
client: RedisClient;
|
|
||||||
clientIndex: number;
|
|
||||||
lastRotationTime: number;
|
|
||||||
lock: boolean;
|
|
||||||
}
|
|
||||||
> = new Map();
|
|
||||||
if (useRedis) {
|
|
||||||
logger.info('Connecting to redis');
|
|
||||||
for (let i = 0; i < REDIS_CLIENTS.length; i++) {
|
|
||||||
redisClients.push(new RedisClient({ prefix: REDIS_CLIENTS[i] }));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < sdkConfig.SPOT_MARKETS.length; i++) {
|
|
||||||
spotMarketRedisMap.set(sdkConfig.SPOT_MARKETS[i].marketIndex, {
|
|
||||||
client: redisClients[0],
|
|
||||||
clientIndex: 0,
|
|
||||||
lastRotationTime: 0,
|
|
||||||
lock: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (let i = 0; i < sdkConfig.PERP_MARKETS.length; i++) {
|
|
||||||
perpMarketRedisMap.set(sdkConfig.PERP_MARKETS[i].marketIndex, {
|
|
||||||
client: redisClients[0],
|
|
||||||
clientIndex: 0,
|
|
||||||
lastRotationTime: 0,
|
|
||||||
lock: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchFromRedis = async (
|
||||||
|
key: string,
|
||||||
|
selectionCriteria: (responses: any) => any
|
||||||
|
): Promise<JSON> => {
|
||||||
|
const redisResponses = await Promise.all(
|
||||||
|
redisClients.map((client) => client.getRaw(key))
|
||||||
|
);
|
||||||
|
return selectionCriteria(redisResponses);
|
||||||
|
};
|
||||||
|
|
||||||
const userMapClient = new RedisClient({
|
const userMapClient = new RedisClient({
|
||||||
host: process.env.ELASTICACHE_USERMAP_HOST ?? process.env.ELASTICACHE_HOST,
|
host: process.env.ELASTICACHE_USERMAP_HOST ?? process.env.ELASTICACHE_HOST,
|
||||||
port: process.env.ELASTICACHE_USERMAP_PORT ?? process.env.ELASTICACHE_PORT,
|
port: process.env.ELASTICACHE_USERMAP_PORT ?? process.env.ELASTICACHE_PORT,
|
||||||
prefix: RedisClientPrefix.USER_MAP,
|
prefix: RedisClientPrefix.USER_MAP,
|
||||||
});
|
});
|
||||||
|
|
||||||
function canRotate(marketType: MarketType, marketIndex: number) {
|
|
||||||
if (isVariant(marketType, 'spot')) {
|
|
||||||
const state = spotMarketRedisMap.get(marketIndex);
|
|
||||||
if (state) {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - state.lastRotationTime > ROTATION_COOLDOWN && !state.lock) {
|
|
||||||
state.lastRotationTime = now;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const state = perpMarketRedisMap.get(marketIndex);
|
|
||||||
if (state) {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - state.lastRotationTime > ROTATION_COOLDOWN && !state.lock) {
|
|
||||||
state.lastRotationTime = now;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateClient(marketType: MarketType, marketIndex: number) {
|
|
||||||
if (isVariant(marketType, 'spot')) {
|
|
||||||
const state = spotMarketRedisMap.get(marketIndex);
|
|
||||||
if (state) {
|
|
||||||
state.lock = true;
|
|
||||||
const nextClientIndex = (state.clientIndex + 1) % redisClients.length;
|
|
||||||
state.client = redisClients[nextClientIndex];
|
|
||||||
state.clientIndex = nextClientIndex;
|
|
||||||
logger.info(
|
|
||||||
`Rotated redis client to index ${nextClientIndex} for spot market ${marketIndex}`
|
|
||||||
);
|
|
||||||
state.lastRotationTime = Date.now();
|
|
||||||
state.lock = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const state = perpMarketRedisMap.get(marketIndex);
|
|
||||||
if (state) {
|
|
||||||
state.lock = true;
|
|
||||||
const nextClientIndex = (state.clientIndex + 1) % redisClients.length;
|
|
||||||
state.client = redisClients[nextClientIndex];
|
|
||||||
state.clientIndex = nextClientIndex;
|
|
||||||
logger.info(
|
|
||||||
`Rotated redis client to index ${nextClientIndex} for perp market ${marketIndex}`
|
|
||||||
);
|
|
||||||
state.lastRotationTime = Date.now();
|
|
||||||
state.lock = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Initializing all market subscribers...`);
|
logger.info(`Initializing all market subscribers...`);
|
||||||
const initAllMarketSubscribersStart = Date.now();
|
const initAllMarketSubscribersStart = Date.now();
|
||||||
MARKET_SUBSCRIBERS = await initializeAllMarketSubscribers(driftClient);
|
MARKET_SUBSCRIBERS = await initializeAllMarketSubscribers(driftClient);
|
||||||
@@ -555,27 +473,22 @@ const main = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let topMakers: string[];
|
let topMakers: string[];
|
||||||
if (useRedis) {
|
|
||||||
const redisClient = isVariant(normedMarketType, 'perp')
|
const redisResponse = await fetchFromRedis(
|
||||||
? perpMarketRedisMap.get(normedMarketIndex).client
|
`last_update_orderbook_best_makers_${getVariant(
|
||||||
: spotMarketRedisMap.get(normedMarketIndex).client;
|
normedMarketType
|
||||||
const redisResponse = await redisClient.getRaw(
|
)}_${marketIndex}`,
|
||||||
`last_update_orderbook_best_makers_${getVariant(
|
selectMostRecentBySlot
|
||||||
normedMarketType
|
);
|
||||||
)}_${marketIndex}`
|
if (redisResponse) {
|
||||||
);
|
if (
|
||||||
if (redisResponse) {
|
dlobProvider.getSlot() - redisResponse['slot'] <
|
||||||
const parsedResponse = JSON.parse(redisResponse);
|
SLOT_STALENESS_TOLERANCE
|
||||||
if (
|
) {
|
||||||
parsedResponse &&
|
if (side === 'bid') {
|
||||||
dlobProvider.getSlot() - parsedResponse.slot <
|
topMakers = redisResponse['bids'];
|
||||||
SLOT_STALENESS_TOLERANCE
|
} else {
|
||||||
) {
|
topMakers = redisResponse['asks'];
|
||||||
if (side === 'bid') {
|
|
||||||
topMakers = parsedResponse.bids;
|
|
||||||
} else {
|
|
||||||
topMakers = parsedResponse.asks;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -729,54 +642,38 @@ const main = async (): Promise<void> => {
|
|||||||
const adjustedDepth = depth ?? '100';
|
const adjustedDepth = depth ?? '100';
|
||||||
|
|
||||||
let l2Formatted: any;
|
let l2Formatted: any;
|
||||||
if (useRedis) {
|
if (!isSpot && `${includeVamm}`?.toLowerCase() === 'true') {
|
||||||
if (!isSpot && `${includeVamm}`?.toLowerCase() === 'true') {
|
const redisL2 = await fetchFromRedis(
|
||||||
const redisClient = perpMarketRedisMap.get(normedMarketIndex).client;
|
`last_update_orderbook_perp_${normedMarketIndex}`,
|
||||||
const redisL2 = await redisClient.get(
|
selectMostRecentBySlot
|
||||||
`last_update_orderbook_perp_${normedMarketIndex}`
|
);
|
||||||
);
|
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
|
||||||
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
|
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
||||||
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
||||||
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
redisL2 &&
|
redisL2 &&
|
||||||
dlobProvider.getSlot() - parseInt(redisL2['slot']) <
|
dlobProvider.getSlot() - redisL2['slot'] < SLOT_STALENESS_TOLERANCE
|
||||||
SLOT_STALENESS_TOLERANCE
|
|
||||||
) {
|
|
||||||
l2Formatted = JSON.stringify(redisL2);
|
|
||||||
} else {
|
|
||||||
if (redisL2 && redisClients.length > 1) {
|
|
||||||
if (canRotate(normedMarketType, normedMarketIndex)) {
|
|
||||||
rotateClient(normedMarketType, normedMarketIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
isSpot &&
|
|
||||||
`${includePhoenix}`?.toLowerCase() === 'true' &&
|
|
||||||
`${includeOpenbook}`?.toLowerCase() === 'true'
|
|
||||||
) {
|
) {
|
||||||
const redisClient = spotMarketRedisMap.get(normedMarketIndex).client;
|
l2Formatted = JSON.stringify(redisL2);
|
||||||
const redisL2 = await redisClient.get(
|
}
|
||||||
`last_update_orderbook_spot_${normedMarketIndex}`
|
} else if (
|
||||||
);
|
isSpot &&
|
||||||
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
|
`${includePhoenix}`?.toLowerCase() === 'true' &&
|
||||||
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
`${includeOpenbook}`?.toLowerCase() === 'true'
|
||||||
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
) {
|
||||||
if (
|
const redisL2 = await fetchFromRedis(
|
||||||
redisL2 &&
|
`last_update_orderbook_spot_${normedMarketIndex}`,
|
||||||
dlobProvider.getSlot() - parseInt(redisL2['slot']) <
|
selectMostRecentBySlot
|
||||||
SLOT_STALENESS_TOLERANCE
|
);
|
||||||
) {
|
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
|
||||||
l2Formatted = JSON.stringify(redisL2);
|
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
||||||
} else {
|
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
||||||
if (redisL2 && redisClients.length > 1) {
|
if (
|
||||||
if (canRotate(normedMarketType, normedMarketIndex)) {
|
redisL2 &&
|
||||||
rotateClient(normedMarketType, normedMarketIndex);
|
dlobProvider.getSlot() - redisL2['slot'] < SLOT_STALENESS_TOLERANCE
|
||||||
}
|
) {
|
||||||
}
|
l2Formatted = JSON.stringify(redisL2);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l2Formatted) {
|
if (l2Formatted) {
|
||||||
@@ -891,65 +788,40 @@ const main = async (): Promise<void> => {
|
|||||||
|
|
||||||
const adjustedDepth = normedParam['depth'] ?? '100';
|
const adjustedDepth = normedParam['depth'] ?? '100';
|
||||||
let l2Formatted: any;
|
let l2Formatted: any;
|
||||||
if (useRedis) {
|
if (!isSpot && normedParam['includeVamm']?.toLowerCase() === 'true') {
|
||||||
if (
|
const redisL2 = await fetchFromRedis(
|
||||||
!isSpot &&
|
`last_update_orderbook_perp_${normedMarketIndex}`,
|
||||||
normedParam['includeVamm']?.toLowerCase() === 'true'
|
selectMostRecentBySlot
|
||||||
) {
|
);
|
||||||
const redisClient =
|
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
|
||||||
perpMarketRedisMap.get(normedMarketIndex).client;
|
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
||||||
const redisL2 = await redisClient.get(
|
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
||||||
`last_update_orderbook_perp_${normedMarketIndex}`
|
if (redisL2) {
|
||||||
);
|
if (
|
||||||
const depth = Math.min(
|
dlobProvider.getSlot() - redisL2['slot'] <
|
||||||
parseInt(adjustedDepth as string) ?? 1,
|
SLOT_STALENESS_TOLERANCE
|
||||||
100
|
) {
|
||||||
);
|
l2Formatted = redisL2;
|
||||||
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
|
||||||
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
|
||||||
if (redisL2) {
|
|
||||||
if (
|
|
||||||
dlobProvider.getSlot() - parseInt(redisL2['slot']) <
|
|
||||||
SLOT_STALENESS_TOLERANCE
|
|
||||||
) {
|
|
||||||
l2Formatted = redisL2;
|
|
||||||
} else {
|
|
||||||
if (redisClients.length > 1) {
|
|
||||||
if (canRotate(normedMarketType, normedMarketIndex)) {
|
|
||||||
rotateClient(normedMarketType, normedMarketIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (
|
}
|
||||||
isSpot &&
|
} else if (
|
||||||
normedParam['includePhoenix']?.toLowerCase() === 'true' &&
|
isSpot &&
|
||||||
normedParam['includeOpenbook']?.toLowerCase() === 'true'
|
normedParam['includePhoenix']?.toLowerCase() === 'true' &&
|
||||||
) {
|
normedParam['includeOpenbook']?.toLowerCase() === 'true'
|
||||||
const redisClient =
|
) {
|
||||||
spotMarketRedisMap.get(normedMarketIndex).client;
|
const redisL2 = await fetchFromRedis(
|
||||||
const redisL2 = await redisClient.get(
|
`last_update_orderbook_spot_${normedMarketIndex}`,
|
||||||
`last_update_orderbook_spot_${normedMarketIndex}`
|
selectMostRecentBySlot
|
||||||
);
|
);
|
||||||
const depth = Math.min(
|
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
|
||||||
parseInt(adjustedDepth as string) ?? 1,
|
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
||||||
100
|
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
||||||
);
|
if (redisL2) {
|
||||||
redisL2['bids'] = redisL2['bids']?.slice(0, depth);
|
if (
|
||||||
redisL2['asks'] = redisL2['asks']?.slice(0, depth);
|
dlobProvider.getSlot() - redisL2['slot'] <
|
||||||
if (redisL2) {
|
SLOT_STALENESS_TOLERANCE
|
||||||
if (
|
) {
|
||||||
dlobProvider.getSlot() - parseInt(redisL2['slot']) <
|
l2Formatted = redisL2;
|
||||||
SLOT_STALENESS_TOLERANCE
|
|
||||||
) {
|
|
||||||
l2Formatted = redisL2;
|
|
||||||
} else {
|
|
||||||
if (redisClients.length > 1) {
|
|
||||||
if (canRotate(normedMarketType, normedMarketIndex)) {
|
|
||||||
rotateClient(normedMarketType, normedMarketIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,31 +905,27 @@ const main = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const marketTypeStr = getVariant(normedMarketType);
|
const marketTypeStr = getVariant(normedMarketType);
|
||||||
if (useRedis) {
|
|
||||||
const redisClient = (
|
const redisL3 = await fetchFromRedis(
|
||||||
marketTypeStr === 'spot' ? spotMarketRedisMap : perpMarketRedisMap
|
`last_update_orderbook_l3_${marketTypeStr}_${normedMarketIndex}`,
|
||||||
).get(normedMarketIndex).client;
|
selectMostRecentBySlot
|
||||||
const redisL3 = await redisClient.getRaw(
|
);
|
||||||
`last_update_orderbook_l3_${marketTypeStr}_${normedMarketIndex}`
|
if (
|
||||||
);
|
redisL3 &&
|
||||||
if (
|
dlobProvider.getSlot() - redisL3['slot'] < SLOT_STALENESS_TOLERANCE
|
||||||
redisL3 &&
|
) {
|
||||||
dlobProvider.getSlot() - parseInt(JSON.parse(redisL3).slot) <
|
cacheHitCounter.add(1, {
|
||||||
SLOT_STALENESS_TOLERANCE
|
miss: false,
|
||||||
) {
|
path: req.baseUrl + req.path,
|
||||||
cacheHitCounter.add(1, {
|
});
|
||||||
miss: false,
|
res.writeHead(200);
|
||||||
path: req.baseUrl + req.path,
|
res.end(JSON.stringify(redisL3));
|
||||||
});
|
return;
|
||||||
res.writeHead(200);
|
} else {
|
||||||
res.end(redisL3);
|
cacheHitCounter.add(1, {
|
||||||
return;
|
miss: true,
|
||||||
} else {
|
path: req.baseUrl + req.path,
|
||||||
cacheHitCounter.add(1, {
|
});
|
||||||
miss: true,
|
|
||||||
path: req.baseUrl + req.path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const l3 = dlobSubscriber.getL3({
|
const l3 = dlobSubscriber.getL3({
|
||||||
|
|||||||
@@ -37,12 +37,10 @@ import {
|
|||||||
} from '../dlob-subscriber/DLOBSubscriberIO';
|
} from '../dlob-subscriber/DLOBSubscriberIO';
|
||||||
import {
|
import {
|
||||||
DLOBProvider,
|
DLOBProvider,
|
||||||
getDLOBProviderFromGrpcOrderSubscriber,
|
|
||||||
getDLOBProviderFromOrderSubscriber,
|
getDLOBProviderFromOrderSubscriber,
|
||||||
getDLOBProviderFromUserMap,
|
getDLOBProviderFromUserMap,
|
||||||
} from '../dlobProvider';
|
} from '../dlobProvider';
|
||||||
import FEATURE_FLAGS from '../utils/featureFlags';
|
import FEATURE_FLAGS from '../utils/featureFlags';
|
||||||
import { GeyserOrderSubscriber } from '../grpc/OrderSubscriberGRPC';
|
|
||||||
import express, { Response, Request } from 'express';
|
import express, { Response, Request } from 'express';
|
||||||
import { handleHealthCheck } from '../core/metrics';
|
import { handleHealthCheck } from '../core/metrics';
|
||||||
import { setGlobalDispatcher, Agent } from 'undici';
|
import { setGlobalDispatcher, Agent } from 'undici';
|
||||||
@@ -76,6 +74,17 @@ const dlobSlotGauge = new Gauge({
|
|||||||
'redisClient',
|
'redisClient',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
const oracleSlotGauge = new Gauge({
|
||||||
|
name: 'oracle_slot',
|
||||||
|
help: 'Last updated slot of oracle',
|
||||||
|
labelNames: [
|
||||||
|
'marketIndex',
|
||||||
|
'marketType',
|
||||||
|
'marketName',
|
||||||
|
'redisPrefix',
|
||||||
|
'redisClient',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const sdkConfig = initialize({ env: process.env.ENV });
|
const sdkConfig = initialize({ env: process.env.ENV });
|
||||||
@@ -116,6 +125,10 @@ const SPOT_MARKETS_TO_LOAD =
|
|||||||
|
|
||||||
logger.info(`RPC endpoint: ${endpoint}`);
|
logger.info(`RPC endpoint: ${endpoint}`);
|
||||||
logger.info(`WS endpoint: ${wsEndpoint}`);
|
logger.info(`WS endpoint: ${wsEndpoint}`);
|
||||||
|
logger.info(`Token: ${token}`);
|
||||||
|
logger.info(
|
||||||
|
`useOrderSubscriber: ${useOrderSubscriber}, useWebsocket: ${useWebsocket}, useGrpc: ${useGrpc}`
|
||||||
|
);
|
||||||
logger.info(`DriftEnv: ${driftEnv}`);
|
logger.info(`DriftEnv: ${driftEnv}`);
|
||||||
logger.info(`Commit: ${commitHash}`);
|
logger.info(`Commit: ${commitHash}`);
|
||||||
|
|
||||||
@@ -377,17 +390,38 @@ const main = async () => {
|
|||||||
|
|
||||||
let dlobProvider: DLOBProvider;
|
let dlobProvider: DLOBProvider;
|
||||||
if (useOrderSubscriber) {
|
if (useOrderSubscriber) {
|
||||||
let subscriptionConfig;
|
let subscriptionConfig: any = {
|
||||||
|
type: 'polling',
|
||||||
|
commitment: stateCommitment,
|
||||||
|
frequency: ORDERBOOK_UPDATE_INTERVAL,
|
||||||
|
};
|
||||||
|
|
||||||
if (useWebsocket) {
|
if (useWebsocket) {
|
||||||
subscriptionConfig = {
|
subscriptionConfig = {
|
||||||
type: 'websocket',
|
type: 'websocket',
|
||||||
commitment: stateCommitment,
|
commitment: stateCommitment,
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// USE_GRPC=true will override websocket
|
||||||
|
if (useGrpc) {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('TOKEN is required for grpc');
|
||||||
|
}
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error('ENDPOINT is required for grpc');
|
||||||
|
}
|
||||||
|
if (useWebsocket) {
|
||||||
|
logger.warn('USE_GRPC overriding USE_WEBSOCKET');
|
||||||
|
}
|
||||||
subscriptionConfig = {
|
subscriptionConfig = {
|
||||||
type: 'polling',
|
type: 'grpc',
|
||||||
|
grpcConfigs: {
|
||||||
|
endpoint: endpoint,
|
||||||
|
token: token,
|
||||||
|
commitmentLevel: stateCommitment,
|
||||||
|
},
|
||||||
commitment: stateCommitment,
|
commitment: stateCommitment,
|
||||||
frequency: ORDERBOOK_UPDATE_INTERVAL,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,17 +435,6 @@ const main = async () => {
|
|||||||
slotSource = {
|
slotSource = {
|
||||||
getSlot: () => orderSubscriber.getSlot(),
|
getSlot: () => orderSubscriber.getSlot(),
|
||||||
};
|
};
|
||||||
} else if (useGrpc) {
|
|
||||||
const grpcOrderSubscriber = new GeyserOrderSubscriber(driftClient, {
|
|
||||||
endpoint: endpoint,
|
|
||||||
token: token,
|
|
||||||
});
|
|
||||||
|
|
||||||
dlobProvider = getDLOBProviderFromGrpcOrderSubscriber(grpcOrderSubscriber);
|
|
||||||
|
|
||||||
slotSource = {
|
|
||||||
getSlot: () => grpcOrderSubscriber.getSlot(),
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const userMap = new UserMap({
|
const userMap = new UserMap({
|
||||||
driftClient,
|
driftClient,
|
||||||
@@ -463,6 +486,9 @@ const main = async () => {
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const slot = slotSource.getSlot();
|
const slot = slotSource.getSlot();
|
||||||
perpMarketInfos.forEach((market) => {
|
perpMarketInfos.forEach((market) => {
|
||||||
|
const oracleDataAndSlot = driftClient.getOracleDataForPerpMarket(
|
||||||
|
market.marketIndex
|
||||||
|
);
|
||||||
dlobSlotGauge.set(
|
dlobSlotGauge.set(
|
||||||
{
|
{
|
||||||
marketIndex: market.marketIndex,
|
marketIndex: market.marketIndex,
|
||||||
@@ -473,8 +499,21 @@ const main = async () => {
|
|||||||
},
|
},
|
||||||
slot
|
slot
|
||||||
);
|
);
|
||||||
|
oracleSlotGauge.set(
|
||||||
|
{
|
||||||
|
marketIndex: market.marketIndex,
|
||||||
|
marketType: 'perp',
|
||||||
|
marketName: market.marketName,
|
||||||
|
redisClient: REDIS_CLIENT,
|
||||||
|
redisPrefix: RedisClientPrefix[REDIS_CLIENT],
|
||||||
|
},
|
||||||
|
oracleDataAndSlot.slot.toNumber()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
spotMarketInfos.forEach((market) => {
|
spotMarketInfos.forEach((market) => {
|
||||||
|
const oracleDataAndSlot = driftClient.getOracleDataForSpotMarket(
|
||||||
|
market.marketIndex
|
||||||
|
);
|
||||||
dlobSlotGauge.set(
|
dlobSlotGauge.set(
|
||||||
{
|
{
|
||||||
marketIndex: market.marketIndex,
|
marketIndex: market.marketIndex,
|
||||||
@@ -483,7 +522,7 @@ const main = async () => {
|
|||||||
redisClient: REDIS_CLIENT,
|
redisClient: REDIS_CLIENT,
|
||||||
redisPrefix: RedisClientPrefix[REDIS_CLIENT],
|
redisPrefix: RedisClientPrefix[REDIS_CLIENT],
|
||||||
},
|
},
|
||||||
slot
|
oracleDataAndSlot.slot.toNumber()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ const main = async () => {
|
|||||||
const redisClient = new RedisClient({ prefix: redisClientPrefix });
|
const redisClient = new RedisClient({ prefix: redisClientPrefix });
|
||||||
await redisClient.connect();
|
await redisClient.connect();
|
||||||
|
|
||||||
const slotSubscriber = new SlotSubscriber(connection, {});
|
const slotSubscriber = new SlotSubscriber(connection, {
|
||||||
|
resubTimeoutMs: 10_000,
|
||||||
|
});
|
||||||
|
|
||||||
const lamportsBalance = await connection.getBalance(wallet.publicKey);
|
const lamportsBalance = await connection.getBalance(wallet.publicKey);
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -472,3 +472,18 @@ export type SubscriberLookup = {
|
|||||||
openbook?: OpenbookV2Subscriber;
|
openbook?: OpenbookV2Subscriber;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectMostRecentBySlot = (responses: any[]): any => {
|
||||||
|
const parsedResponses = responses
|
||||||
|
.map((response) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(response);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((parsed) => parsed && typeof parsed.slot === 'number');
|
||||||
|
return parsedResponses.reduce((mostRecent, current) => {
|
||||||
|
return !mostRecent || current.slot > mostRecent.slot ? current : mostRecent;
|
||||||
|
}, null);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import express from 'express';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import compression from 'compression';
|
import compression from 'compression';
|
||||||
import { WebSocket, WebSocketServer } from 'ws';
|
import { WebSocket, WebSocketServer } from 'ws';
|
||||||
import { sleep } from './utils/utils';
|
import { sleep, selectMostRecentBySlot } from './utils/utils';
|
||||||
import { register, Gauge } from 'prom-client';
|
import { register, Gauge, Counter } from 'prom-client';
|
||||||
import { DriftEnv, PerpMarkets, SpotMarkets } from '@drift-labs/sdk';
|
import { DriftEnv, PerpMarkets, SpotMarkets } from '@drift-labs/sdk';
|
||||||
import { RedisClient, RedisClientPrefix } from '@drift/common/clients';
|
import { RedisClient, RedisClientPrefix } from '@drift/common/clients';
|
||||||
|
|
||||||
@@ -21,6 +21,16 @@ const wsConnectionsGauge = new Gauge({
|
|||||||
name: 'websocket_connections',
|
name: 'websocket_connections',
|
||||||
help: 'Number of active WebSocket connections',
|
help: 'Number of active WebSocket connections',
|
||||||
});
|
});
|
||||||
|
const wsOrderbookSourceCounter = new Counter({
|
||||||
|
name: 'websocket_orderbook_source',
|
||||||
|
help: 'Number of orderbook messages sent from source',
|
||||||
|
labelNames: ['source'],
|
||||||
|
});
|
||||||
|
const wsOrderbookSourceLastSlotGauge = new Gauge({
|
||||||
|
name: 'websocket_orderbook_source_last_slot',
|
||||||
|
help: 'Last slot of orderbook messages from a source',
|
||||||
|
labelNames: ['source'],
|
||||||
|
});
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const wss = new WebSocketServer({
|
const wss = new WebSocketServer({
|
||||||
@@ -34,15 +44,27 @@ const WS_PORT = process.env.WS_PORT || '3000';
|
|||||||
console.log(`WS LISTENER PORT : ${WS_PORT}`);
|
console.log(`WS LISTENER PORT : ${WS_PORT}`);
|
||||||
|
|
||||||
const MAX_BUFFERED_AMOUNT = 300000;
|
const MAX_BUFFERED_AMOUNT = 300000;
|
||||||
const REDIS_CLIENT = process.env.REDIS_CLIENT || 'DLOB';
|
|
||||||
const CHANNEL_PREFIX = RedisClientPrefix[REDIS_CLIENT];
|
|
||||||
console.log('Redis Clients:', REDIS_CLIENT);
|
|
||||||
const CHANNEL_PREFIX_HELIUS = RedisClientPrefix.DLOB_HELIUS;
|
|
||||||
|
|
||||||
|
const envClients = [];
|
||||||
|
const clients = process.env.REDIS_CLIENT?.trim()
|
||||||
|
.replace(/^\[|\]$/g, '')
|
||||||
|
.split(/\s*,\s*/);
|
||||||
|
|
||||||
|
clients?.forEach((client) => envClients.push(RedisClientPrefix[client]));
|
||||||
|
|
||||||
|
const REDIS_CLIENTS = envClients.length
|
||||||
|
? envClients
|
||||||
|
: [RedisClientPrefix.DLOB, RedisClientPrefix.DLOB_HELIUS];
|
||||||
|
console.log('Redis Clients:', REDIS_CLIENTS);
|
||||||
|
|
||||||
|
const regexp = new RegExp(REDIS_CLIENTS.join('|'), 'g');
|
||||||
const sanitiseChannelForClient = (channel: string | undefined): string => {
|
const sanitiseChannelForClient = (channel: string | undefined): string => {
|
||||||
return channel
|
return channel?.replace(regexp, '');
|
||||||
?.replace(CHANNEL_PREFIX, '')
|
};
|
||||||
?.replace(CHANNEL_PREFIX_HELIUS, '');
|
const getChannelPrefix = (channel: string | undefined): string | undefined => {
|
||||||
|
if (!channel) return undefined;
|
||||||
|
const match = channel.match(regexp);
|
||||||
|
return match?.[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRedisChannelFromMessage = (message: any): string => {
|
const getRedisChannelFromMessage = (message: any): string => {
|
||||||
@@ -70,11 +92,11 @@ const getRedisChannelFromMessage = (message: any): string => {
|
|||||||
|
|
||||||
switch (channel.toLowerCase()) {
|
switch (channel.toLowerCase()) {
|
||||||
case 'trades':
|
case 'trades':
|
||||||
return `${CHANNEL_PREFIX}trades_${marketType}_${marketIndex}`;
|
return `trades_${marketType}_${marketIndex}`;
|
||||||
case 'orderbook':
|
case 'orderbook':
|
||||||
return `${CHANNEL_PREFIX}orderbook_${marketType}_${marketIndex}`;
|
return `orderbook_${marketType}_${marketIndex}`;
|
||||||
case 'priorityfees':
|
case 'priorityfees':
|
||||||
return `${CHANNEL_PREFIX_HELIUS}priorityFees_${marketType}_${marketIndex}`;
|
return `priorityFees_${marketType}_${marketIndex}`;
|
||||||
case undefined:
|
case undefined:
|
||||||
default:
|
default:
|
||||||
throw new Error('Bad channel specified');
|
throw new Error('Bad channel specified');
|
||||||
@@ -82,47 +104,71 @@ const getRedisChannelFromMessage = (message: any): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const redisClient = new RedisClient({});
|
const subscribedChannelToSlot: Map<string, number> = new Map();
|
||||||
const lastMessageRetriever = new RedisClient({ prefix: CHANNEL_PREFIX });
|
|
||||||
|
|
||||||
await redisClient.connect();
|
const redisClients: Array<RedisClient> = [];
|
||||||
await lastMessageRetriever.connect();
|
for (let i = 0; i < REDIS_CLIENTS.length; i++) {
|
||||||
|
const redisClient = new RedisClient({ prefix: REDIS_CLIENTS[i] });
|
||||||
|
redisClients.push(redisClient);
|
||||||
|
|
||||||
|
redisClient.forceGetClient().on('connect', () => {
|
||||||
|
subscribedChannels.forEach(async (channel) => {
|
||||||
|
try {
|
||||||
|
await redisClient.subscribe(channel);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error subscribing to ${channel}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.forceGetClient().on('message', (subscribedChannel, message) => {
|
||||||
|
const sanitizedChannel = sanitiseChannelForClient(subscribedChannel);
|
||||||
|
const channelPrefix = getChannelPrefix(sanitizedChannel);
|
||||||
|
const subscribers = channelSubscribers.get(sanitizedChannel);
|
||||||
|
if (subscribers) {
|
||||||
|
if (sanitizedChannel.includes('orderbook')) {
|
||||||
|
const messageSlot = JSON.parse(message)['slot'];
|
||||||
|
wsOrderbookSourceLastSlotGauge.set(
|
||||||
|
{
|
||||||
|
source: channelPrefix,
|
||||||
|
},
|
||||||
|
messageSlot
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastMessageSlot = subscribedChannelToSlot.get(sanitizedChannel);
|
||||||
|
if (!lastMessageSlot || lastMessageSlot <= messageSlot) {
|
||||||
|
subscribedChannelToSlot.set(sanitizedChannel, messageSlot);
|
||||||
|
} else if (lastMessageSlot > messageSlot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subscribers.forEach((ws) => {
|
||||||
|
if (
|
||||||
|
ws.readyState === WebSocket.OPEN &&
|
||||||
|
ws.bufferedAmount < MAX_BUFFERED_AMOUNT
|
||||||
|
) {
|
||||||
|
wsOrderbookSourceCounter.inc({
|
||||||
|
source: channelPrefix,
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
channel: sanitizedChannel,
|
||||||
|
data: message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.forceGetClient().on('error', (error) => {
|
||||||
|
console.error('Redis client error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const channelSubscribers = new Map<string, Set<WebSocket>>();
|
const channelSubscribers = new Map<string, Set<WebSocket>>();
|
||||||
const subscribedChannels = new Set<string>();
|
const subscribedChannels = new Set<string>();
|
||||||
|
|
||||||
redisClient.forceGetClient().on('connect', () => {
|
|
||||||
subscribedChannels.forEach(async (channel) => {
|
|
||||||
try {
|
|
||||||
await redisClient.subscribe(channel);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error subscribing to ${channel}:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
redisClient.forceGetClient().on('message', (subscribedChannel, message) => {
|
|
||||||
const subscribers = channelSubscribers.get(subscribedChannel);
|
|
||||||
if (subscribers) {
|
|
||||||
subscribers.forEach((ws) => {
|
|
||||||
if (
|
|
||||||
ws.readyState === WebSocket.OPEN &&
|
|
||||||
ws.bufferedAmount < MAX_BUFFERED_AMOUNT
|
|
||||||
)
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
channel: sanitiseChannelForClient(subscribedChannel),
|
|
||||||
data: message,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
redisClient.forceGetClient().on('error', (error) => {
|
|
||||||
console.error('Redis client error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
wss.on('connection', (ws: WebSocket) => {
|
wss.on('connection', (ws: WebSocket) => {
|
||||||
console.log('Client connected');
|
console.log('Client connected');
|
||||||
wsConnectionsGauge.inc();
|
wsConnectionsGauge.inc();
|
||||||
@@ -166,8 +212,12 @@ async function main() {
|
|||||||
|
|
||||||
if (!subscribedChannels.has(redisChannel)) {
|
if (!subscribedChannels.has(redisChannel)) {
|
||||||
console.log('Trying to subscribe to channel', redisChannel);
|
console.log('Trying to subscribe to channel', redisChannel);
|
||||||
redisClient
|
redisClients[0]
|
||||||
.subscribe(redisChannel)
|
.subscribe(
|
||||||
|
redisChannel.includes('priorityFees')
|
||||||
|
? `dlob-helius:${redisChannel}`
|
||||||
|
: `${redisClients[0].getPrefix()}${redisChannel}`
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
subscribedChannels.add(redisChannel);
|
subscribedChannels.add(redisChannel);
|
||||||
})
|
})
|
||||||
@@ -179,6 +229,21 @@ async function main() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (redisChannel.includes('orderbook') && redisClients.length > 1) {
|
||||||
|
redisClients.slice(1).map((redisClient) => {
|
||||||
|
redisClient
|
||||||
|
.subscribe(`${redisClient.getPrefix()}${redisChannel}`)
|
||||||
|
.catch(() => {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
error: `Error subscribing to channel: ${parsedMessage}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channelSubscribers.get(redisChannel)) {
|
if (!channelSubscribers.get(redisChannel)) {
|
||||||
@@ -194,17 +259,21 @@ async function main() {
|
|||||||
);
|
);
|
||||||
// Fetch and send last message
|
// Fetch and send last message
|
||||||
if (redisChannel.includes('orderbook')) {
|
if (redisChannel.includes('orderbook')) {
|
||||||
const lastUpdateChannel = `last_update_${redisChannel}`.replace(
|
const lastMessages = await Promise.all(
|
||||||
CHANNEL_PREFIX,
|
redisClients.map((redisClient) =>
|
||||||
''
|
redisClient.getRaw(
|
||||||
);
|
`last_update_${redisChannel}`.replace(
|
||||||
const lastMessage = await lastMessageRetriever.getRaw(
|
redisClient.getPrefix(),
|
||||||
lastUpdateChannel
|
''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
const lastMessage = selectMostRecentBySlot(lastMessages);
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
channel: lastUpdateChannel,
|
channel: redisChannel,
|
||||||
data: lastMessage,
|
data: lastMessage,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -278,7 +347,7 @@ async function main() {
|
|||||||
clearInterval(bufferInterval);
|
clearInterval(bufferInterval);
|
||||||
channelSubscribers.forEach((subscribers, channel) => {
|
channelSubscribers.forEach((subscribers, channel) => {
|
||||||
if (subscribers.delete(ws) && subscribers.size === 0) {
|
if (subscribers.delete(ws) && subscribers.size === 0) {
|
||||||
redisClient.unsubscribe(channel);
|
redisClients.map((redisClient) => redisClient.unsubscribe(channel));
|
||||||
channelSubscribers.delete(channel);
|
channelSubscribers.delete(channel);
|
||||||
subscribedChannels.delete(channel);
|
subscribedChannels.delete(channel);
|
||||||
}
|
}
|
||||||
|
|||||||
105
yarn.lock
105
yarn.lock
@@ -287,13 +287,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.11"
|
regenerator-runtime "^0.13.11"
|
||||||
|
|
||||||
"@babel/runtime@^7.23.2":
|
|
||||||
version "7.23.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db"
|
|
||||||
integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime "^0.14.0"
|
|
||||||
|
|
||||||
"@babel/runtime@^7.24.6", "@babel/runtime@^7.24.8":
|
"@babel/runtime@^7.24.6", "@babel/runtime@^7.24.8":
|
||||||
version "7.24.8"
|
version "7.24.8"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e"
|
||||||
@@ -460,7 +453,7 @@
|
|||||||
kuler "^2.0.0"
|
kuler "^2.0.0"
|
||||||
|
|
||||||
"@drift-labs/sdk@file:./drift-common/protocol/sdk", "@drift-labs/sdk@file:drift-common/protocol/sdk":
|
"@drift-labs/sdk@file:./drift-common/protocol/sdk", "@drift-labs/sdk@file:drift-common/protocol/sdk":
|
||||||
version "2.103.0-beta.8"
|
version "2.103.0-beta.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@coral-xyz/anchor" "0.28.0"
|
"@coral-xyz/anchor" "0.28.0"
|
||||||
"@coral-xyz/anchor-30" "npm:@coral-xyz/anchor@0.30.1"
|
"@coral-xyz/anchor-30" "npm:@coral-xyz/anchor@0.30.1"
|
||||||
@@ -487,7 +480,7 @@
|
|||||||
"@drift/common@file:./drift-common/common-ts":
|
"@drift/common@file:./drift-common/common-ts":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@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"
|
"@drift-labs/sdk" "file:../../Library/Caches/Yarn/v6/npm-@drift-common-1.0.0-736eee55-37f6-4ebd-9a13-95d148c9fb80-1733261412306/node_modules/@drift/protocol/sdk"
|
||||||
"@jest/globals" "^29.3.1"
|
"@jest/globals" "^29.3.1"
|
||||||
"@slack/web-api" "^6.4.0"
|
"@slack/web-api" "^6.4.0"
|
||||||
"@solana/spl-token" "^0.3.8"
|
"@solana/spl-token" "^0.3.8"
|
||||||
@@ -891,13 +884,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@noble/hashes" "1.4.0"
|
"@noble/hashes" "1.4.0"
|
||||||
|
|
||||||
"@noble/curves@^1.2.0":
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
|
|
||||||
integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
|
|
||||||
dependencies:
|
|
||||||
"@noble/hashes" "1.3.2"
|
|
||||||
|
|
||||||
"@noble/curves@^1.4.0":
|
"@noble/curves@^1.4.0":
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6"
|
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6"
|
||||||
@@ -915,11 +901,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123"
|
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123"
|
||||||
integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==
|
integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==
|
||||||
|
|
||||||
"@noble/hashes@1.3.2", "@noble/hashes@^1.3.1":
|
|
||||||
version "1.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
|
||||||
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
|
||||||
|
|
||||||
"@noble/hashes@1.4.0", "@noble/hashes@^1.3.0", "@noble/hashes@^1.4.0":
|
"@noble/hashes@1.4.0", "@noble/hashes@^1.3.0", "@noble/hashes@^1.4.0":
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
|
||||||
@@ -930,6 +911,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.3.tgz#360afc77610e0a61f3417e497dcf36862e4f8111"
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.3.tgz#360afc77610e0a61f3417e497dcf36862e4f8111"
|
||||||
integrity sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==
|
integrity sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==
|
||||||
|
|
||||||
|
"@noble/hashes@^1.3.1":
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
|
||||||
|
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
|
||||||
|
|
||||||
"@noble/secp256k1@^1.6.3":
|
"@noble/secp256k1@^1.6.3":
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1"
|
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1"
|
||||||
@@ -1956,6 +1942,27 @@
|
|||||||
rpc-websockets "^8.0.1"
|
rpc-websockets "^8.0.1"
|
||||||
superstruct "^1.0.4"
|
superstruct "^1.0.4"
|
||||||
|
|
||||||
|
"@solana/web3.js@1.95.2", "@solana/web3.js@^1.54.0", "@solana/web3.js@^1.77.3", "@solana/web3.js@^1.93.0", "@solana/web3.js@^1.95.0":
|
||||||
|
version "1.95.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.2.tgz#6f8a0362fa75886a21550dbec49aad54481463a6"
|
||||||
|
integrity sha512-SjlHp0G4qhuhkQQc+YXdGkI8EerCqwxvgytMgBpzMUQTafrkNant3e7pgilBGgjy/iM40ICvWBLgASTPMrQU7w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.24.8"
|
||||||
|
"@noble/curves" "^1.4.2"
|
||||||
|
"@noble/hashes" "^1.4.0"
|
||||||
|
"@solana/buffer-layout" "^4.0.1"
|
||||||
|
agentkeepalive "^4.5.0"
|
||||||
|
bigint-buffer "^1.1.5"
|
||||||
|
bn.js "^5.2.1"
|
||||||
|
borsh "^0.7.0"
|
||||||
|
bs58 "^4.0.1"
|
||||||
|
buffer "6.0.3"
|
||||||
|
fast-stable-stringify "^1.0.0"
|
||||||
|
jayson "^4.1.1"
|
||||||
|
node-fetch "^2.7.0"
|
||||||
|
rpc-websockets "^9.0.2"
|
||||||
|
superstruct "^2.0.2"
|
||||||
|
|
||||||
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.30.2":
|
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.30.2":
|
||||||
version "1.67.0"
|
version "1.67.0"
|
||||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.67.0.tgz#bd67742cd9c176889fd1954080173dd7d0c224a2"
|
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.67.0.tgz#bd67742cd9c176889fd1954080173dd7d0c224a2"
|
||||||
@@ -1999,48 +2006,6 @@
|
|||||||
rpc-websockets "^7.5.1"
|
rpc-websockets "^7.5.1"
|
||||||
superstruct "^0.14.2"
|
superstruct "^0.14.2"
|
||||||
|
|
||||||
"@solana/web3.js@^1.54.0", "@solana/web3.js@^1.77.3", "@solana/web3.js@^1.93.0", "@solana/web3.js@^1.95.0":
|
|
||||||
version "1.95.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.2.tgz#6f8a0362fa75886a21550dbec49aad54481463a6"
|
|
||||||
integrity sha512-SjlHp0G4qhuhkQQc+YXdGkI8EerCqwxvgytMgBpzMUQTafrkNant3e7pgilBGgjy/iM40ICvWBLgASTPMrQU7w==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.24.8"
|
|
||||||
"@noble/curves" "^1.4.2"
|
|
||||||
"@noble/hashes" "^1.4.0"
|
|
||||||
"@solana/buffer-layout" "^4.0.1"
|
|
||||||
agentkeepalive "^4.5.0"
|
|
||||||
bigint-buffer "^1.1.5"
|
|
||||||
bn.js "^5.2.1"
|
|
||||||
borsh "^0.7.0"
|
|
||||||
bs58 "^4.0.1"
|
|
||||||
buffer "6.0.3"
|
|
||||||
fast-stable-stringify "^1.0.0"
|
|
||||||
jayson "^4.1.1"
|
|
||||||
node-fetch "^2.7.0"
|
|
||||||
rpc-websockets "^9.0.2"
|
|
||||||
superstruct "^2.0.2"
|
|
||||||
|
|
||||||
"@solana/web3.js@^1.73.3":
|
|
||||||
version "1.87.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.87.6.tgz#6744cfc5f4fc81e0f58241c0a92648a7320bb3bf"
|
|
||||||
integrity sha512-LkqsEBgTZztFiccZZXnawWa8qNCATEqE97/d0vIwjTclmVlc8pBpD1DmjfVHtZ1HS5fZorFlVhXfpwnCNDZfyg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.23.2"
|
|
||||||
"@noble/curves" "^1.2.0"
|
|
||||||
"@noble/hashes" "^1.3.1"
|
|
||||||
"@solana/buffer-layout" "^4.0.0"
|
|
||||||
agentkeepalive "^4.3.0"
|
|
||||||
bigint-buffer "^1.1.5"
|
|
||||||
bn.js "^5.2.1"
|
|
||||||
borsh "^0.7.0"
|
|
||||||
bs58 "^4.0.1"
|
|
||||||
buffer "6.0.3"
|
|
||||||
fast-stable-stringify "^1.0.0"
|
|
||||||
jayson "^4.1.0"
|
|
||||||
node-fetch "^2.6.12"
|
|
||||||
rpc-websockets "^7.5.1"
|
|
||||||
superstruct "^0.14.2"
|
|
||||||
|
|
||||||
"@solana/web3.js@^1.90.0":
|
"@solana/web3.js@^1.90.0":
|
||||||
version "1.95.1"
|
version "1.95.1"
|
||||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.1.tgz#fcbbaf845309ff7ceb8d3726702799e8c27530e8"
|
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.1.tgz#fcbbaf845309ff7ceb8d3726702799e8c27530e8"
|
||||||
@@ -5106,13 +5071,6 @@ node-fetch@2, node-fetch@2.6.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
node-fetch@^2.6.12, node-fetch@^2.7.0:
|
|
||||||
version "2.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
|
||||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
|
||||||
dependencies:
|
|
||||||
whatwg-url "^5.0.0"
|
|
||||||
|
|
||||||
node-fetch@^2.6.7:
|
node-fetch@^2.6.7:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
|
||||||
@@ -5120,6 +5078,13 @@ node-fetch@^2.6.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
|
node-fetch@^2.7.0:
|
||||||
|
version "2.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||||
|
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||||
|
dependencies:
|
||||||
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
node-gyp-build@^4.3.0:
|
node-gyp-build@^4.3.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
|
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
|
||||||
|
|||||||
Reference in New Issue
Block a user