From df09f4126821b2038b23dde77ca2bdfebf8507ca Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Mon, 16 Oct 2023 14:14:25 -0700 Subject: [PATCH 1/7] protoype bot --- jitMaker.config.yaml | 12 +- src/bots/uncrossArbBot.ts | 239 ++++++++++++++++++++++++++++++++++++++ src/config.ts | 11 ++ src/index.ts | 134 +++++++++++---------- 4 files changed, 333 insertions(+), 63 deletions(-) create mode 100644 src/bots/uncrossArbBot.ts diff --git a/jitMaker.config.yaml b/jitMaker.config.yaml index 100d20e5..838cb3cb 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,8 +63,16 @@ 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: + uncrossArb: + botId: "uncrossArb" + dryRun: false + driftEnv: "mainnet-beta" + jitMaker: botId: "jitMaker" dryRun: false diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts new file mode 100644 index 00000000..d494ca4e --- /dev/null +++ b/src/bots/uncrossArbBot.ts @@ -0,0 +1,239 @@ +/* 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, + 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 = new UserMap( + this.driftClient, + this.driftClient.userAccountSubscriptionConfig, + false + ); + 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.userMap.subscribe(); + 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 = PerpMarkets[this.driftEnv]; + for (let i = 0; i < marketIndexes.length; i++) { + const perpIdx = marketIndexes[i].marketIndex; + const driftUser = this.driftClient.getUser(0); + 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: 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 + ) { + await this.jitProxyClient.arbPerp({ + marketIndex: perpIdx, + makerInfos: [bidMakerInfo, askMakerInfo], + }); + } + } + + 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/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..bcce9e8e 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,9 @@ import { RetryTxSender, AuctionSubscriber, FastSingleTxSender, + OracleInfo, } 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 +60,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 +123,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 +171,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 +180,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 +241,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 +279,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 +310,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 +361,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 +387,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 +411,9 @@ const runBot = async () => { return; } + /** + * Jito info here + */ let jitoSearcherClient: SearcherClient | undefined; let jitoAuthKeypair: Keypair | undefined; if (config.global.useJito) { @@ -490,6 +467,7 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'fillerLite')) { logger.info(`Starting filler lite bot`); bots.push( @@ -510,6 +488,7 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'spotFiller')) { bots.push( new SpotFillerBot( @@ -528,6 +507,7 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'trigger')) { bots.push( new TriggerBot( @@ -544,10 +524,11 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'jitMaker')) { const jitProxyClient = new JitProxyClient({ driftClient, - programId: new PublicKey('J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP'), + programId: new PublicKey(sdkConfig.JIT_PROXY_PROGRAM_ID!), }); const auctionSubscriber = new AuctionSubscriber({ driftClient }); @@ -604,6 +585,7 @@ const runBot = async () => { ) ); } + if (configHasBot(config, 'floatingMaker')) { bots.push( new FloatingPerpMakerBot( @@ -642,6 +624,31 @@ const runBot = async () => { ); } + if (configHasBot(config, 'fundingRateUpdater')) { + bots.push( + new FundingRateUpdaterBot( + driftClient, + config.botConfigs!.fundingRateUpdater! + ) + ); + } + + if (configHasBot(config, 'uncrossArb')) { + const jitProxyClient = new JitProxyClient({ + driftClient, + programId: new PublicKey(sdkConfig.JIT_PROXY_PROGRAM_ID!), + }); + bots.push( + new UncrossArbBot( + driftClient, + jitProxyClient, + slotSubscriber, + config.botConfigs!.uncrossArb!, + config.global.driftEnv! + ) + ); + } + logger.info(`initializing bots`); await Promise.all(bots.map((bot) => bot.init())); @@ -672,24 +679,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 +722,8 @@ const runBot = async () => { } }; +recursiveTryCatch(() => runBot()); + async function recursiveTryCatch(f: () => void) { try { f(); @@ -747,4 +738,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 + )}` + ); +} From 561bc650d662d19bbaba50f9f3b310b1e5028ed8 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Mon, 16 Oct 2023 14:20:04 -0700 Subject: [PATCH 2/7] small change --- src/bots/uncrossArbBot.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts index d494ca4e..1e4c0a34 100644 --- a/src/bots/uncrossArbBot.ts +++ b/src/bots/uncrossArbBot.ts @@ -141,7 +141,7 @@ export class UncrossArbBot implements Bot { const marketIndexes = PerpMarkets[this.driftEnv]; for (let i = 0; i < marketIndexes.length; i++) { const perpIdx = marketIndexes[i].marketIndex; - const driftUser = this.driftClient.getUser(0); + const driftUser = this.driftClient.getUser(); const perpMarketAccount = this.driftClient.getPerpMarketAccount(perpIdx)!; const oraclePriceData = @@ -184,9 +184,12 @@ export class UncrossArbBot implements Bot { .getUserAccount(), order: bestDriftBid.order, maker: bestDriftBid.userAccount!, - makerStats: this.userMap - .get(bestDriftBid.userAccount!.toBase58())! - .getUserAccount().authority, + makerStats: getUserStatsAccountPublicKey( + this.driftClient.program.programId, + this.userMap + .get(bestDriftBid.userAccount!.toBase58())! + .getUserAccount().authority + ), }; const askMakerInfo: MakerInfo = { @@ -200,7 +203,7 @@ export class UncrossArbBot implements Bot { this.userMap .get(bestDriftAsk.userAccount!.toBase58())! .getUserAccount().authority - ), + ) }; const midPrice = (bestBidPrice + bestAskPrice) / 2; From 86a26de4cae9a6b66c7c8235244cabca98e5b33e Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Mon, 16 Oct 2023 14:20:28 -0700 Subject: [PATCH 3/7] prettify --- src/bots/uncrossArbBot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts index 1e4c0a34..ea5914a6 100644 --- a/src/bots/uncrossArbBot.ts +++ b/src/bots/uncrossArbBot.ts @@ -203,7 +203,7 @@ export class UncrossArbBot implements Bot { this.userMap .get(bestDriftAsk.userAccount!.toBase58())! .getUserAccount().authority - ) + ), }; const midPrice = (bestBidPrice + bestAskPrice) / 2; From c1f9a37a331ca798335c7ecce831f538dbe02cf0 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Mon, 16 Oct 2023 15:21:24 -0700 Subject: [PATCH 4/7] create userMap in index.ts --- jitMaker.config.yaml | 2 +- src/bots/filler.ts | 17 +++++++++++------ src/bots/fillerLite.ts | 1 + src/bots/jitMaker.ts | 8 ++------ src/bots/liquidator.ts | 11 +++-------- src/bots/makerBidAskTwapCrank.ts | 12 ++---------- src/bots/spotFiller.ts | 11 +++-------- src/bots/trigger.ts | 10 +++------- src/bots/uncrossArbBot.ts | 8 ++------ src/bots/userPnlSettler.ts | 17 +++++++---------- src/index.ts | 28 ++++++++++++++++++++++++++-- 11 files changed, 61 insertions(+), 64 deletions(-) diff --git a/jitMaker.config.yaml b/jitMaker.config.yaml index 838cb3cb..56f0e83f 100644 --- a/jitMaker.config.yaml +++ b/jitMaker.config.yaml @@ -74,7 +74,7 @@ botConfigs: driftEnv: "mainnet-beta" jitMaker: - botId: "jitMaker" + botId: "liquidator" 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] 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 index ea5914a6..be0c787b 100644 --- a/src/bots/uncrossArbBot.ts +++ b/src/bots/uncrossArbBot.ts @@ -57,6 +57,7 @@ export class UncrossArbBot implements Bot { driftClient: DriftClient, // driftClient needs to have correct number of subaccounts listed jitProxyClient: JitProxyClient, slotSubscriber: SlotSubscriber, + userMap: UserMap, config: BaseBotConfig, driftEnv: DriftEnv ) { @@ -66,12 +67,8 @@ export class UncrossArbBot implements Bot { this.dryRun = config.dryRun; this.driftEnv = driftEnv; this.slotSubscriber = slotSubscriber; + this.userMap = userMap; - this.userMap = new UserMap( - this.driftClient, - this.driftClient.userAccountSubscriptionConfig, - false - ); this.dlobSubscriber = new DLOBSubscriber({ dlobSource: this.userMap, slotSource: this.slotSubscriber, @@ -86,7 +83,6 @@ export class UncrossArbBot implements Bot { public async init(): Promise { logger.info(`${this.name} initing`); - await this.userMap.subscribe(); await this.dlobSubscriber.subscribe(); logger.info(`${this.name} init done`); 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/index.ts b/src/index.ts index bcce9e8e..13964fd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { AuctionSubscriber, FastSingleTxSender, OracleInfo, + UserMap, } from '@drift-labs/sdk'; import { promiseTimeout } from '@drift-labs/sdk/lib/util/promiseTimeout'; @@ -446,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,11 +498,13 @@ const runBot = async () => { } if (configHasBot(config, 'spotFiller')) { + await userMap.subscribe(); bots.push( new SpotFillerBot( slotSubscriber, bulkAccountLoader, driftClient, + userMap, eventSubscriber, { rpcEndpoint: endpoint, @@ -509,10 +519,12 @@ const runBot = async () => { } if (configHasBot(config, 'trigger')) { + await userMap.subscribe(); bots.push( new TriggerBot( driftClient, slotSubscriber, + userMap, { rpcEndpoint: endpoint, commit: commitHash, @@ -526,6 +538,7 @@ const runBot = async () => { } if (configHasBot(config, 'jitMaker')) { + await userMap.subscribe(); const jitProxyClient = new JitProxyClient({ driftClient, programId: new PublicKey(sdkConfig.JIT_PROXY_PROGRAM_ID!), @@ -551,6 +564,7 @@ const runBot = async () => { new JitMaker( driftClient, jitter, + userMap, config.botConfigs!.jitMaker!, config.global.driftEnv! ) @@ -558,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 ) @@ -570,9 +585,11 @@ const runBot = async () => { } if (configHasBot(config, 'liquidator')) { + await userMap.subscribe(); bots.push( new LiquidatorBot( driftClient, + userMap, { rpcEndpoint: endpoint, commit: commitHash, @@ -604,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 + ) ); } @@ -634,6 +656,7 @@ const runBot = async () => { } if (configHasBot(config, 'uncrossArb')) { + await userMap.subscribe(); const jitProxyClient = new JitProxyClient({ driftClient, programId: new PublicKey(sdkConfig.JIT_PROXY_PROGRAM_ID!), @@ -643,6 +666,7 @@ const runBot = async () => { driftClient, jitProxyClient, slotSubscriber, + userMap, config.botConfigs!.uncrossArb!, config.global.driftEnv! ) From 14980b175641374714c8fcda7a89cee3b0803c8c Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Tue, 17 Oct 2023 11:15:29 -0700 Subject: [PATCH 5/7] add try/except --- src/bots/uncrossArbBot.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts index be0c787b..72f7d70c 100644 --- a/src/bots/uncrossArbBot.ts +++ b/src/bots/uncrossArbBot.ts @@ -207,10 +207,21 @@ export class UncrossArbBot implements Bot { (bestBidPrice - bestAskPrice) / midPrice > 2 * driftUser.getMarketFees(MarketType.PERP, perpIdx).takerFee ) { - await this.jitProxyClient.arbPerp({ - marketIndex: perpIdx, - makerInfos: [bidMakerInfo, askMakerInfo], - }); + try { + const txResult = await this.jitProxyClient.arbPerp({ + marketIndex: perpIdx, + makerInfos: [bidMakerInfo, askMakerInfo], + }); + logger.info(`Completed arb with sig: ${txResult.txSig}`); + } 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 + }` + ); + } + } } } From 401861422e0612dcbfef25b45938664c59cd226d Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Tue, 17 Oct 2023 15:06:15 -0700 Subject: [PATCH 6/7] prettify --- jitMaker.config.yaml | 19 ++++++++++--------- src/bots/uncrossArbBot.ts | 4 +++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/jitMaker.config.yaml b/jitMaker.config.yaml index 56f0e83f..8d170934 100644 --- a/jitMaker.config.yaml +++ b/jitMaker.config.yaml @@ -68,17 +68,18 @@ enabledBots: # below are bot configs botConfigs: + # 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 driftEnv: "mainnet-beta" - jitMaker: - botId: "liquidator" - 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] diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts index 72f7d70c..7ccfaee9 100644 --- a/src/bots/uncrossArbBot.ts +++ b/src/bots/uncrossArbBot.ts @@ -212,7 +212,9 @@ export class UncrossArbBot implements Bot { marketIndex: perpIdx, makerInfos: [bidMakerInfo, askMakerInfo], }); - logger.info(`Completed arb with sig: ${txResult.txSig}`); + logger.info( + `Potential arb with sig: ${txResult.txSig}. Check the blockchain for confirmation.` + ); } catch (e) { if (e instanceof Error) { logger.error( From ecfe026fc0dfc6744d1fb98458e6a9c112b9dad0 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Tue, 17 Oct 2023 15:26:14 -0700 Subject: [PATCH 7/7] use getPerpMarketAccounts() --- src/bots/uncrossArbBot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bots/uncrossArbBot.ts b/src/bots/uncrossArbBot.ts index 7ccfaee9..1436a859 100644 --- a/src/bots/uncrossArbBot.ts +++ b/src/bots/uncrossArbBot.ts @@ -134,7 +134,7 @@ export class UncrossArbBot implements Bot { console.log( `[${new Date().toISOString()}] Running uncross periodic tasks...` ); - const marketIndexes = PerpMarkets[this.driftEnv]; + const marketIndexes = this.driftClient.getPerpMarketAccounts(); for (let i = 0; i < marketIndexes.length; i++) { const perpIdx = marketIndexes[i].marketIndex; const driftUser = this.driftClient.getUser();