diff --git a/jitMaker.config.yaml b/jitMaker.config.yaml index 100d20e5..8d170934 100644 --- a/jitMaker.config.yaml +++ b/jitMaker.config.yaml @@ -6,7 +6,7 @@ global: endpoint: # custom websocket endpoint to use (if null will be determined from rpc endpoint) wsEndpoint: - resubTimeoutMs: 30000 # timeout for resubscribing to websocket + resubTimeoutMs: 10000 # timeout for resubscribing to websocket # Private key to use to sign transactions. # will load from KEEPER_PRIVATE_KEY env var if null @@ -52,7 +52,7 @@ enabledBots: # - liquidator # Example maker bot that participates in JIT auction (caution: you will probably lose money) - - jitMaker + # - jitMaker # Example maker bot that posts floating oracle orders # - floatingMaker @@ -63,14 +63,23 @@ enabledBots: # settles negative PnLs for users (may want to run with runOnce: true) # - userPnlSettler + # uncross the book to capture an arb + - uncrossArb + # below are bot configs botConfigs: - jitMaker: - botId: "jitMaker" + # jitMaker: + # botId: "jitMaker" + # dryRun: false + # # below, ordering is important: match the subaccountIds to perpMarketindices. + # # e.g. to MM perp markets 0, 1 both on subaccount 0, then subaccounts=[0,0], perpMarketIndicies=[0,1] + # # to MM perp market 0 on subaccount 0 and perp market 1 on subaccount 1, then subaccounts=[0, 1], perpMarketIndicies=[0, 1] + # # also, make sure all subaccounts are loaded in the global config subaccounts above to avoid errors + # subaccounts: [0] + # perpMarketIndicies: [0] + + uncrossArb: + botId: "uncrossArb" dryRun: false - # below, ordering is important: match the subaccountIds to perpMarketindices. - # e.g. to MM perp markets 0, 1 both on subaccount 0, then subaccounts=[0,0], perpMarketIndicies=[0,1] - # to MM perp market 0 on subaccount 0 and perp market 1 on subaccount 1, then subaccounts=[0, 1], perpMarketIndicies=[0, 1] - # also, make sure all subaccounts are loaded in the global config subaccounts above to avoid errors - subaccounts: [0] - perpMarketIndicies: [0] + driftEnv: "mainnet-beta" + diff --git a/src/bots/filler.ts b/src/bots/filler.ts index 00914db5..d88132e0 100644 --- a/src/bots/filler.ts +++ b/src/bots/filler.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ReferrerInfo, isOracleValid, @@ -250,6 +251,7 @@ export class FillerBot implements Bot { slotSubscriber: SlotSubscriber, bulkAccountLoader: BulkAccountLoader | undefined, driftClient: DriftClient, + userMap: UserMap | undefined, eventSubscriber: EventSubscriber | undefined, runtimeSpec: RuntimeSpec, config: FillerConfig, @@ -284,6 +286,7 @@ export class FillerBot implements Bot { if (this.metricsPort) { this.initializeMetrics(); } + this.userMap = userMap; this.transactionVersion = config.transactionVersion ?? undefined; logger.info( @@ -522,12 +525,14 @@ export class FillerBot implements Bot { public async init() { logger.info(`${this.name} initing`); - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); - await this.userMap.subscribe(); + if (!this.userMap) { + this.userMap = new UserMap( + this.driftClient, + this.driftClient.userAccountSubscriptionConfig, + false + ); + await this.userMap.subscribe(); + } this.userStatsMap = new UserStatsMap( this.driftClient, diff --git a/src/bots/fillerLite.ts b/src/bots/fillerLite.ts index 910145a3..f4446e6c 100644 --- a/src/bots/fillerLite.ts +++ b/src/bots/fillerLite.ts @@ -37,6 +37,7 @@ export class FillerLiteBot extends FillerBot { undefined, driftClient, undefined, + undefined, runtimeSpec, config, jitoSearcherClient, diff --git a/src/bots/jitMaker.ts b/src/bots/jitMaker.ts index 5140393d..24448ccb 100644 --- a/src/bots/jitMaker.ts +++ b/src/bots/jitMaker.ts @@ -90,6 +90,7 @@ export class JitMaker implements Bot { constructor( driftClient: DriftClient, // driftClient needs to have correct number of subaccounts listed jitter: JitterSniper | JitterShotgun, + userMap: UserMap, config: JitMakerConfig, driftEnv: DriftEnv ) { @@ -103,12 +104,8 @@ export class JitMaker implements Bot { this.name = config.botId; this.dryRun = config.dryRun; this.driftEnv = driftEnv; + this.userMap = userMap; - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); this.slotSubscriber = new SlotSubscriber(this.driftClient.connection); this.dlobSubscriber = new DLOBSubscriber({ dlobSource: this.userMap, @@ -125,7 +122,6 @@ export class JitMaker implements Bot { logger.info(`${this.name} initing`); // do stuff that takes some time - await this.userMap.subscribe(); await this.slotSubscriber.subscribe(); await this.dlobSubscriber.subscribe(); diff --git a/src/bots/liquidator.ts b/src/bots/liquidator.ts index 65d45380..a58e0387 100644 --- a/src/bots/liquidator.ts +++ b/src/bots/liquidator.ts @@ -284,7 +284,7 @@ export class LiquidatorBot implements Bot { private perpMarketToSubaccount: Map; private spotMarketToSubaccount: Map; private intervalIds: Array = []; - private userMap?: UserMap; + private userMap: UserMap; private deriskMutex = new Uint8Array(new SharedArrayBuffer(1)); private runtimeSpecs: RuntimeSpec; private serumFulfillmentConfigMap: SerumFulfillmentConfigMap; @@ -305,6 +305,7 @@ export class LiquidatorBot implements Bot { constructor( driftClient: DriftClient, + userMap: UserMap, runtimeSpec: RuntimeSpec, config: LiquidatorConfig, defaultSubaccountId: number @@ -317,6 +318,7 @@ export class LiquidatorBot implements Bot { this.serumFulfillmentConfigMap = new SerumFulfillmentConfigMap( this.driftClient ); + this.userMap = userMap; this.metricsPort = config.metricsPort; if (this.metricsPort) { @@ -537,12 +539,6 @@ export class LiquidatorBot implements Bot { public async init() { logger.info(`${this.name} initing`); - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); - await this.userMap.subscribe(); const config = initialize({ env: this.runtimeSpecs.driftEnv as DriftEnv }); for (const spotMarketConfig of config.SPOT_MARKETS) { @@ -1249,7 +1245,6 @@ export class LiquidatorBot implements Bot { * attempts to close out any open positions on this account. It starts by cancelling any open orders */ private async derisk() { - console.log('wtfwtfwtf'); if (!this.acquireMutex()) { return; } diff --git a/src/bots/makerBidAskTwapCrank.ts b/src/bots/makerBidAskTwapCrank.ts index 3407e6f3..8882fb95 100644 --- a/src/bots/makerBidAskTwapCrank.ts +++ b/src/bots/makerBidAskTwapCrank.ts @@ -1,7 +1,6 @@ import { DLOB, DriftClient, - DriftEnv, UserMap, SlotSubscriber, MarketType, @@ -72,7 +71,7 @@ export class MakerBidAskTwapCrank implements Bot { constructor( driftClient: DriftClient, slotSubscriber: SlotSubscriber, - driftEnv: DriftEnv, + userMap: UserMap, config: BaseBotConfig, runOnce: boolean ) { @@ -81,20 +80,13 @@ export class MakerBidAskTwapCrank implements Bot { this.dryRun = config.dryRun; this.runOnce = runOnce; this.driftClient = driftClient; + this.userMap = userMap; } public async init() { logger.info(`${this.name} initing, runOnce: ${this.runOnce}`); this.lookupTableAccount = await this.driftClient.fetchMarketLookupTableAccount(); - - // initialize userMap instance - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); - await this.userMap.subscribe(); } public async reset() { diff --git a/src/bots/spotFiller.ts b/src/bots/spotFiller.ts index 29594f08..78382b46 100644 --- a/src/bots/spotFiller.ts +++ b/src/bots/spotFiller.ts @@ -145,7 +145,7 @@ export class SpotFillerBot implements Bot { private dlobSubscriber?: DLOBSubscriber; - private userMap?: UserMap; + private userMap: UserMap; private userStatsMap?: UserStatsMap; private serumFulfillmentConfigMap: SerumFulfillmentConfigMap; @@ -189,6 +189,7 @@ export class SpotFillerBot implements Bot { slotSubscriber: SlotSubscriber, bulkAccountLoader: BulkAccountLoader | undefined, driftClient: DriftClient, + userMap: UserMap, eventSubscriber: EventSubscriber, runtimeSpec: RuntimeSpec, config: FillerConfig @@ -203,6 +204,7 @@ export class SpotFillerBot implements Bot { this.slotSubscriber = slotSubscriber; this.driftClient = driftClient; this.eventSubscriber = eventSubscriber; + this.userMap = userMap; this.bulkAccountLoader = bulkAccountLoader; if (this.bulkAccountLoader) { this.userStatsMapSubscriptionConfig = { @@ -386,13 +388,6 @@ export class SpotFillerBot implements Bot { const initPromises: Array> = []; - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); - initPromises.push(this.userMap.subscribe()); - this.userStatsMap = new UserStatsMap( this.driftClient, this.userStatsMapSubscriptionConfig diff --git a/src/bots/trigger.ts b/src/bots/trigger.ts index ffd45115..2cb8db24 100644 --- a/src/bots/trigger.ts +++ b/src/bots/trigger.ts @@ -54,7 +54,7 @@ export class TriggerBot implements Bot { private triggeringNodes = new Map(); private periodicTaskMutex = new Mutex(); private intervalIds: Array = []; - private userMap?: UserMap; + private userMap: UserMap; private priorityFeeCalculator: PriorityFeeCalculator; @@ -76,12 +76,14 @@ export class TriggerBot implements Bot { constructor( driftClient: DriftClient, slotSubscriber: SlotSubscriber, + userMap: UserMap, runtimeSpec: RuntimeSpec, config: BaseBotConfig ) { this.name = config.botId; this.dryRun = config.dryRun; this.driftClient = driftClient; + this.userMap = userMap; this.runtimeSpec = runtimeSpec; this.slotSubscriber = slotSubscriber; @@ -157,12 +159,6 @@ export class TriggerBot implements Bot { public async init() { logger.info(`${this.name} initing`); - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); - await this.userMap.subscribe(); this.dlobSubscriber = new DLOBSubscriber({ dlobSource: this.userMap, diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts new file mode 100644 index 00000000..1436a859 --- /dev/null +++ b/src/bots/uncrossArbBot.ts @@ -0,0 +1,251 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + DriftEnv, + DriftClient, + convertToNumber, + MarketType, + PRICE_PRECISION, + DLOBSubscriber, + UserMap, + SlotSubscriber, + UserStatsMap, + MakerInfo, + PerpMarkets, + getUserStatsAccountPublicKey, +} from '@drift-labs/sdk'; +import { Mutex, tryAcquire, E_ALREADY_LOCKED } from 'async-mutex'; +import { logger } from '../logger'; +import { Bot } from '../types'; +import { + getBestLimitAskExcludePubKey, + getBestLimitBidExcludePubKey, + sleepMs, +} from '../utils'; +import { JitProxyClient } from '@drift-labs/jit-proxy/lib'; +import dotenv = require('dotenv'); + +dotenv.config(); +import { BaseBotConfig } from 'src/config'; + +const TARGET_LEVERAGE_PER_ACCOUNT = 1; + +/** + * This is an example of a bot that implements the Bot interface. + */ +export class UncrossArbBot implements Bot { + public readonly name: string; + public readonly dryRun: boolean; + public readonly defaultIntervalMs: number = 1000; + + private driftEnv: DriftEnv; + private periodicTaskMutex = new Mutex(); + + private jitProxyClient: JitProxyClient; + private driftClient: DriftClient; + // private subaccountConfig: SubaccountConfig; + private intervalIds: Array = []; + + private watchdogTimerMutex = new Mutex(); + private watchdogTimerLastPatTime = Date.now(); + + private dlobSubscriber: DLOBSubscriber; + private slotSubscriber: SlotSubscriber; + private userMap: UserMap; + + constructor( + driftClient: DriftClient, // driftClient needs to have correct number of subaccounts listed + jitProxyClient: JitProxyClient, + slotSubscriber: SlotSubscriber, + userMap: UserMap, + config: BaseBotConfig, + driftEnv: DriftEnv + ) { + this.jitProxyClient = jitProxyClient; + this.driftClient = driftClient; + this.name = config.botId; + this.dryRun = config.dryRun; + this.driftEnv = driftEnv; + this.slotSubscriber = slotSubscriber; + this.userMap = userMap; + + this.dlobSubscriber = new DLOBSubscriber({ + dlobSource: this.userMap, + slotSource: this.slotSubscriber, + updateFrequency: 1000, + driftClient: this.driftClient, + }); + } + + /** + * Run initialization procedures for the bot. + */ + public async init(): Promise { + logger.info(`${this.name} initing`); + + await this.dlobSubscriber.subscribe(); + + logger.info(`${this.name} init done`); + } + + /** + * Reset the bot - usually you will reset any periodic tasks here + */ + public async reset(): Promise { + // reset any periodic tasks + for (const intervalId of this.intervalIds) { + clearInterval(intervalId as NodeJS.Timeout); + } + this.intervalIds = []; + } + + public async startIntervalLoop(intervalMs: number): Promise { + const intervalId = setInterval( + this.runPeriodicTasks.bind(this), + intervalMs + ); + this.intervalIds.push(intervalId); + + logger.info(`${this.name} Bot started! driftEnv: ${this.driftEnv}`); + } + + /** + * Typically used for monitoring liveness. + * @returns true if bot is healthy, else false. + */ + public async healthCheck(): Promise { + let healthy = false; + await this.watchdogTimerMutex.runExclusive(async () => { + healthy = + this.watchdogTimerLastPatTime > Date.now() - 2 * this.defaultIntervalMs; + }); + return healthy; + } + + /** + * Typical bot loop that runs periodically and pats the watchdog timer on completion. + * + */ + private async runPeriodicTasks() { + const start = Date.now(); + let ran = false; + try { + await tryAcquire(this.periodicTaskMutex).runExclusive(async () => { + console.log( + `[${new Date().toISOString()}] Running uncross periodic tasks...` + ); + const marketIndexes = this.driftClient.getPerpMarketAccounts(); + for (let i = 0; i < marketIndexes.length; i++) { + const perpIdx = marketIndexes[i].marketIndex; + const driftUser = this.driftClient.getUser(); + const perpMarketAccount = + this.driftClient.getPerpMarketAccount(perpIdx)!; + const oraclePriceData = + this.driftClient.getOracleDataForPerpMarket(perpIdx); + + const bestDriftBid = getBestLimitBidExcludePubKey( + this.dlobSubscriber.dlob, + perpMarketAccount.marketIndex, + MarketType.PERP, + oraclePriceData.slot.toNumber(), + oraclePriceData, + driftUser.userAccountPublicKey + ); + + const bestDriftAsk = getBestLimitAskExcludePubKey( + this.dlobSubscriber.dlob, + perpMarketAccount.marketIndex, + MarketType.PERP, + oraclePriceData.slot.toNumber(), + oraclePriceData, + driftUser.userAccountPublicKey + ); + if (!bestDriftBid || !bestDriftAsk) { + return; + } + + const currentSlot = this.slotSubscriber.getSlot(); + const bestBidPrice = convertToNumber( + bestDriftBid.getPrice(oraclePriceData, currentSlot), + PRICE_PRECISION + ); + const bestAskPrice = convertToNumber( + bestDriftAsk.getPrice(oraclePriceData, currentSlot), + PRICE_PRECISION + ); + + const bidMakerInfo: MakerInfo = { + makerUserAccount: this.userMap + .get(bestDriftBid.userAccount!.toBase58())! + .getUserAccount(), + order: bestDriftBid.order, + maker: bestDriftBid.userAccount!, + makerStats: getUserStatsAccountPublicKey( + this.driftClient.program.programId, + this.userMap + .get(bestDriftBid.userAccount!.toBase58())! + .getUserAccount().authority + ), + }; + + const askMakerInfo: MakerInfo = { + makerUserAccount: this.userMap + .get(bestDriftAsk.userAccount!.toBase58())! + .getUserAccount(), + order: bestDriftAsk.order, + maker: bestDriftAsk.userAccount!, + makerStats: getUserStatsAccountPublicKey( + this.driftClient.program.programId, + this.userMap + .get(bestDriftAsk.userAccount!.toBase58())! + .getUserAccount().authority + ), + }; + + const midPrice = (bestBidPrice + bestAskPrice) / 2; + if ( + (bestBidPrice - bestAskPrice) / midPrice > + 2 * driftUser.getMarketFees(MarketType.PERP, perpIdx).takerFee + ) { + try { + const txResult = await this.jitProxyClient.arbPerp({ + marketIndex: perpIdx, + makerInfos: [bidMakerInfo, askMakerInfo], + }); + logger.info( + `Potential arb with sig: ${txResult.txSig}. Check the blockchain for confirmation.` + ); + } catch (e) { + if (e instanceof Error) { + logger.error( + `Error sending arb tx on market index ${perpIdx} with detected market ${bestBidPrice}@${bestAskPrice}: ${ + e.stack ? e.stack : e.message + }` + ); + } + } + } + } + + await sleepMs(100); + console.log(`done: ${Date.now() - start}ms`); + ran = true; + }); + } catch (e) { + if (e === E_ALREADY_LOCKED) { + return; + } else { + throw e; + } + } finally { + if (ran) { + const duration = Date.now() - start; + logger.debug(`${this.name} Bot took ${duration}ms to run`); + + await this.watchdogTimerMutex.runExclusive(async () => { + this.watchdogTimerLastPatTime = Date.now(); + }); + } + } + } +} diff --git a/src/bots/userPnlSettler.ts b/src/bots/userPnlSettler.ts index 483d0286..f417af89 100644 --- a/src/bots/userPnlSettler.ts +++ b/src/bots/userPnlSettler.ts @@ -63,16 +63,21 @@ export class UserPnlSettlerBot implements Bot { private driftClient: DriftClient; private lookupTableAccount?: AddressLookupTableAccount; private intervalIds: Array = []; - private userMap?: UserMap; + private userMap: UserMap; private watchdogTimerMutex = new Mutex(); private watchdogTimerLastPatTime = Date.now(); - constructor(driftClient: DriftClient, config: BaseBotConfig) { + constructor( + driftClient: DriftClient, + config: BaseBotConfig, + userMap: UserMap + ) { this.name = config.botId; this.dryRun = config.dryRun; this.runOnce = config.runOnce || false; this.driftClient = driftClient; + this.userMap = userMap; } public async init() { @@ -80,14 +85,6 @@ export class UserPnlSettlerBot implements Bot { this.lookupTableAccount = await this.driftClient.fetchMarketLookupTableAccount(); - - // initialize userMap instance - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); - await this.userMap.sync(); } public async reset() { diff --git a/src/config.ts b/src/config.ts index b4aa2e6e..d4dac64b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -53,6 +53,7 @@ export type BotConfigMap = { fundingRateUpdater?: BaseBotConfig; userPnlSettler?: BaseBotConfig; markTwapCrank?: BaseBotConfig; + uncrossArb?: BaseBotConfig; }; export interface GlobalConfig { @@ -306,6 +307,16 @@ export function loadConfigFromOpts(opts: any): Config { }; } + if (opts.uncrossArb) { + config.enabledBots.push('uncrossArb'); + config.botConfigs!.uncrossArb = { + dryRun: opts.dryRun ?? false, + botId: process.env.BOT_ID ?? 'uncrossArb', + metricsPort: 9464, + runOnce: opts.runOnce ?? false, + }; + } + return mergeDefaults(defaultConfig, config) as Config; } diff --git a/src/index.ts b/src/index.ts index cdf58756..13964fd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { program, Option } from 'commander'; import * as http from 'http'; @@ -26,9 +27,10 @@ import { RetryTxSender, AuctionSubscriber, FastSingleTxSender, + OracleInfo, + UserMap, } from '@drift-labs/sdk'; import { promiseTimeout } from '@drift-labs/sdk/lib/util/promiseTimeout'; -import { Mutex } from 'async-mutex'; import { logger, setLogLevel } from './logger'; import { constants } from './types'; @@ -59,6 +61,7 @@ import { FundingRateUpdaterBot } from './bots/fundingRateUpdater'; import { FillerLiteBot } from './bots/fillerLite'; import { JitProxyClient, JitterSniper } from '@drift-labs/jit-proxy/lib'; import { MakerBidAskTwapCrank } from './bots/makerBidAskTwapCrank'; +import { UncrossArbBot } from './bots/uncrossArbBot'; require('dotenv').config(); const commitHash = process.env.COMMIT ?? ''; @@ -121,12 +124,12 @@ program ) .option( '--perp-markets ', - 'comma delimited list of perp market ID(s) to liquidate (willing to inherit risk), omit for all', + 'comma delimited list of perp market ID(s) for applicable bots (willing to inherit risk), omit for all', '' ) .option( '--spot-markets ', - 'comma delimited list of spot market ID(s) to liquidate (willing to inherit risk), omit for all', + 'comma delimited list of spot market ID(s) for applicable bots (willing to inherit risk), omit for all', '' ) .option( @@ -169,7 +172,6 @@ logger.info( // @ts-ignore const sdkConfig = initialize({ env: config.global.driftEnv }); - setLogLevel(config.global.debug ? 'debug' : 'info'); const endpoint = config.global.endpoint!; @@ -179,29 +181,6 @@ logger.info(`WS endpoint: ${wsEndpoint}`); logger.info(`DriftEnv: ${config.global.driftEnv}`); logger.info(`Commit: ${commitHash}`); -function printUserAccountStats(clearingHouseUser: User) { - const freeCollateral = clearingHouseUser.getFreeCollateral(); - logger.info( - `User free collateral: $${convertToNumber( - freeCollateral, - QUOTE_PRECISION - )}:` - ); - - logger.info( - `CHUser unrealized funding PnL: ${convertToNumber( - clearingHouseUser.getUnrealizedFundingPNL(), - QUOTE_PRECISION - )}` - ); - logger.info( - `CHUser unrealized PnL: ${convertToNumber( - clearingHouseUser.getUnrealizedPNL(), - QUOTE_PRECISION - )}` - ); -} - const bots: Bot[] = []; const runBot = async () => { logger.info(`Loading wallet keypair`); @@ -263,13 +242,18 @@ const runBot = async () => { opts, retrySleep: config.global.txRetryTimeoutMs, }); - txSender.getTimeoutCount; - let perpMarketIndexes, spotMarketIndexes, oracleInfos; + /** + * Creating and subscribing to the drift client + */ + let perpMarketIndexes: number[] | undefined; + let spotMarketIndexes: number[] | undefined; + let oracleInfos: OracleInfo[] | undefined; if (configHasBot(config, 'fillerLite')) { ({ perpMarketIndexes, spotMarketIndexes, oracleInfos } = getMarketsAndOraclesForSubscription(config.global.driftEnv!)); } + const driftClient = new DriftClient({ connection, wallet, @@ -296,9 +280,6 @@ const runBot = async () => { }); const slotSubscriber = new SlotSubscriber(connection, {}); - const lastSlotReceivedMutex = new Mutex(); - let lastSlotReceived: number; - let lastHealthCheckSlot = -1; const startupTime = Date.now(); const lamportsBalance = await connection.getBalance(wallet.publicKey); @@ -330,18 +311,12 @@ const runBot = async () => { : [driftUser.subscribe(), eventSubscriber.subscribe()]; await waitForAllSubscribesToFinish(subscribePromises); - // await driftClient.subscribe(); driftClient.eventEmitter.on('error', (e) => { logger.info('clearing house error'); logger.error(e); }); await slotSubscriber.subscribe(); - slotSubscriber.eventEmitter.on('newSlot', async (slot: number) => { - await lastSlotReceivedMutex.runExclusive(async () => { - lastSlotReceived = slot; - }); - }); if (!(await driftClient.getUser().exists())) { logger.error(`User for ${wallet.publicKey} does not exist`); @@ -387,7 +362,9 @@ const runBot = async () => { console.log(`Closed ${closedSpots} spot positions`); } - // check that user has collateral + /** + * Look for collateral and force deposit before running if flag is set + */ const freeCollateral = driftUser.getFreeCollateral(); if ( freeCollateral.isZero() && @@ -411,9 +388,7 @@ const runBot = async () => { } const mint = SpotMarkets[config.global.driftEnv!][0].mint; // TODO: are index 0 always USDC???, support other collaterals - const ata = await getAssociatedTokenAddress(mint, wallet.publicKey); - const amount = new BN(config.global.forceDeposit).mul(QUOTE_PRECISION); if (config.global.driftEnv === 'devnet') { @@ -437,6 +412,9 @@ const runBot = async () => { return; } + /** + * Jito info here + */ let jitoSearcherClient: SearcherClient | undefined; let jitoAuthKeypair: Keypair | undefined; if (config.global.useJito) { @@ -469,12 +447,19 @@ const runBot = async () => { * Start bots depending on flags enabled */ + const userMap = new UserMap( + driftClient, + driftClient.userAccountSubscriptionConfig, + false + ); if (configHasBot(config, 'filler')) { + await userMap.subscribe(); bots.push( new FillerBot( slotSubscriber, bulkAccountLoader, driftClient, + userMap, eventSubscriber, { rpcEndpoint: endpoint, @@ -490,6 +475,7 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'fillerLite')) { logger.info(`Starting filler lite bot`); bots.push( @@ -510,12 +496,15 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'spotFiller')) { + await userMap.subscribe(); bots.push( new SpotFillerBot( slotSubscriber, bulkAccountLoader, driftClient, + userMap, eventSubscriber, { rpcEndpoint: endpoint, @@ -528,11 +517,14 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'trigger')) { + await userMap.subscribe(); bots.push( new TriggerBot( driftClient, slotSubscriber, + userMap, { rpcEndpoint: endpoint, commit: commitHash, @@ -544,10 +536,12 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'jitMaker')) { + await userMap.subscribe(); const jitProxyClient = new JitProxyClient({ driftClient, - programId: new PublicKey('J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP'), + programId: new PublicKey(sdkConfig.JIT_PROXY_PROGRAM_ID!), }); const auctionSubscriber = new AuctionSubscriber({ driftClient }); @@ -570,6 +564,7 @@ const runBot = async () => { new JitMaker( driftClient, jitter, + userMap, config.botConfigs!.jitMaker!, config.global.driftEnv! ) @@ -577,11 +572,12 @@ const runBot = async () => { } if (configHasBot(config, 'markTwapCrank')) { + await userMap.subscribe(); bots.push( new MakerBidAskTwapCrank( driftClient, slotSubscriber, - config.global.driftEnv!, + userMap, config.botConfigs!.markTwapCrank!, config.global.runOnce ?? false ) @@ -589,9 +585,11 @@ const runBot = async () => { } if (configHasBot(config, 'liquidator')) { + await userMap.subscribe(); bots.push( new LiquidatorBot( driftClient, + userMap, { rpcEndpoint: endpoint, commit: commitHash, @@ -604,6 +602,7 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'floatingMaker')) { bots.push( new FloatingPerpMakerBot( @@ -622,8 +621,13 @@ const runBot = async () => { } if (configHasBot(config, 'userPnlSettler')) { + await userMap.subscribe(); bots.push( - new UserPnlSettlerBot(driftClient, config.botConfigs!.userPnlSettler!) + new UserPnlSettlerBot( + driftClient, + config.botConfigs!.userPnlSettler!, + userMap + ) ); } @@ -642,6 +646,33 @@ const runBot = async () => { ); } + if (configHasBot(config, 'fundingRateUpdater')) { + bots.push( + new FundingRateUpdaterBot( + driftClient, + config.botConfigs!.fundingRateUpdater! + ) + ); + } + + if (configHasBot(config, 'uncrossArb')) { + await userMap.subscribe(); + const jitProxyClient = new JitProxyClient({ + driftClient, + programId: new PublicKey(sdkConfig.JIT_PROXY_PROGRAM_ID!), + }); + bots.push( + new UncrossArbBot( + driftClient, + jitProxyClient, + slotSubscriber, + userMap, + config.botConfigs!.uncrossArb!, + config.global.driftEnv! + ) + ); + } + logger.info(`initializing bots`); await Promise.all(bots.map((bot) => bot.init())); @@ -672,24 +703,6 @@ const runBot = async () => { } } - // check if a slot was received recently - let healthySlotSubscriber = false; - await lastSlotReceivedMutex.runExclusive(async () => { - healthySlotSubscriber = lastSlotReceived > lastHealthCheckSlot; - logger.debug( - `Health check: lastSlotReceived: ${lastSlotReceived}, lastHealthCheckSlot: ${lastHealthCheckSlot}, healthySlot: ${healthySlotSubscriber}` - ); - if (healthySlotSubscriber) { - lastHealthCheckSlot = lastSlotReceived; - } - }); - if (!healthySlotSubscriber) { - res.writeHead(501); - logger.error(`SlotSubscriber is not healthy`); - res.end(`SlotSubscriber is not healthy`); - return; - } - if (bulkAccountLoader) { // we expect health checks to happen at a rate slower than the BulkAccountLoader's polling frequency if ( @@ -733,6 +746,8 @@ const runBot = async () => { } }; +recursiveTryCatch(() => runBot()); + async function recursiveTryCatch(f: () => void) { try { f(); @@ -747,4 +762,25 @@ async function recursiveTryCatch(f: () => void) { } } -recursiveTryCatch(() => runBot()); +function printUserAccountStats(clearingHouseUser: User) { + const freeCollateral = clearingHouseUser.getFreeCollateral(); + logger.info( + `User free collateral: $${convertToNumber( + freeCollateral, + QUOTE_PRECISION + )}:` + ); + + logger.info( + `CHUser unrealized funding PnL: ${convertToNumber( + clearingHouseUser.getUnrealizedFundingPNL(), + QUOTE_PRECISION + )}` + ); + logger.info( + `CHUser unrealized PnL: ${convertToNumber( + clearingHouseUser.getUnrealizedPNL(), + QUOTE_PRECISION + )}` + ); +}