diff --git a/src/bots/filler.ts b/src/bots/filler.ts index c558689f..cee86c27 100644 --- a/src/bots/filler.ts +++ b/src/bots/filler.ts @@ -32,6 +32,10 @@ import { QUOTE_PRECISION, ClockSubscriber, DriftEnv, + OraclePriceData, + StateAccount, + getUserStatsAccountPublicKey, + PositionDirection, PerpMarkets, } from '@drift-labs/sdk'; import { Mutex, tryAcquire, E_ALREADY_LOCKED } from 'async-mutex'; @@ -97,6 +101,7 @@ import { LRUCache } from 'lru-cache'; import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PythPriceFeedSubscriber } from '../pythPriceFeedSubscriber'; import { TxThreaded } from './common/txThreaded'; +import { NodeToTriggerWithMakers } from '../experimental-bots/filler-common/types'; import { PythLazerSubscriber } from '../pythLazerSubscriber'; const TX_COUNT_COOLDOWN_ON_BURST = 10; // send this many tx before resetting burst mode @@ -118,6 +123,7 @@ export const CACHED_BLOCKHASH_OFFSET = 5; const DUMP_TXS_IN_SIM = false; const EXPIRE_ORDER_BUFFER_SEC = 60; // add extra time before trying to expire orders (want to avoid 6252 error due to clock drift) +const NUM_MAKERS = 4; // number of makers to pull for triggerable orders const errorCodesToSuppress = [ 6004, // 0x1774 Error Number: 6004. Error Message: SufficientCollateral. @@ -775,7 +781,7 @@ export class FillerBot extends TxThreaded implements Bot { dlob: DLOB ): { nodesToFill: Array; - nodesToTrigger: Array; + nodesToTrigger: Array; } { const marketIndex = market.marketIndex; @@ -786,6 +792,14 @@ export class FillerBot extends TxThreaded implements Bot { const vBid = calculateBidPrice(market, oraclePriceData); const fillSlot = this.getMaxSlot(); + const nodesToTrigger = this.findTriggerableNodesWithMakers( + dlob, + marketIndex, + fillSlot, + oraclePriceData, + MarketType.PERP, + this.driftClient.getStateAccount() + ); return { nodesToFill: dlob.findNodesToFill( @@ -799,13 +813,7 @@ export class FillerBot extends TxThreaded implements Bot { this.driftClient.getStateAccount(), this.driftClient.getPerpMarketAccount(marketIndex)! ), - nodesToTrigger: dlob.findNodesToTrigger( - marketIndex, - fillSlot, - oraclePriceData.price, - MarketType.PERP, - this.driftClient.getStateAccount() - ), + nodesToTrigger, }; } @@ -1881,10 +1889,10 @@ export class FillerBot extends TxThreaded implements Bot { protected filterPerpNodesForMarket( fillableNodes: Array, - triggerableNodes: Array + triggerableNodes: Array ): { filteredFillableNodes: Array; - filteredTriggerableNodes: Array; + filteredTriggerableNodes: Array; } { const seenFillableNodes = new Set(); const filteredFillableNodes = fillableNodes.filter((node) => { @@ -1920,7 +1928,7 @@ export class FillerBot extends TxThreaded implements Bot { } protected async executeTriggerablePerpNodesForMarket( - triggerableNodes: Array, + triggerableNodes: Array, buildForBundle: boolean ) { for (const nodeToTrigger of triggerableNodes) { @@ -1929,9 +1937,11 @@ export class FillerBot extends TxThreaded implements Bot { nodeToTrigger.node.userAccount.toString() ); logger.info( - `trying to trigger (account: ${nodeToTrigger.node.userAccount.toString()}, slot: ${ + `Processing node: account=${nodeToTrigger.node.userAccount.toString()}, slot=${ user.slot - }) order ${nodeToTrigger.node.order.orderId.toString()}` + }, orderId=${nodeToTrigger.node.order.orderId.toString()}, numMakers=${ + nodeToTrigger.makers?.length ?? 0 + }` ); const nodeSignature = getNodeToTriggerSignature(nodeToTrigger); @@ -1970,6 +1980,36 @@ export class FillerBot extends TxThreaded implements Bot { ) ); + logger.info(`Triggrable node has ${nodeToTrigger.makers.length} makers`); + const makerInfos = await Promise.all( + nodeToTrigger.makers.map(async (m) => { + const maker = new PublicKey(m); + logger.info(`Getting maker info for ${maker.toString()}`); + const { data } = await this.getUserAccountAndSlotFromMap( + maker.toString() + ); + const makerUserAccount = data; + const makerAuthority = makerUserAccount.authority; + const makerStats = getUserStatsAccountPublicKey( + this.driftClient.program.programId, + makerAuthority + ); + return { + maker, + makerStats, + makerUserAccount, + }; + }) + ); + + const fillIx = await this.driftClient.getFillPerpOrderIx( + new PublicKey(nodeToTrigger.node.userAccount), + user.data, + nodeToTrigger.node.order, + makerInfos + ); + ixs.push(fillIx); + if (this.revertOnFailure) { ixs.push(await this.driftClient.getRevertFillIx()); } @@ -2296,6 +2336,51 @@ export class FillerBot extends TxThreaded implements Bot { return true; } + public findTriggerableNodesWithMakers( + dlob: DLOB, + marketIndex: number, + slot: number, + oraclePriceData: OraclePriceData, + marketType: MarketType, + stateAccount: StateAccount + ): NodeToTriggerWithMakers[] { + const baseTriggerable = dlob.findNodesToTrigger( + marketIndex, + slot, + oraclePriceData.price, + marketType, + stateAccount + ); + if (baseTriggerable.length > 0) { + logger.info( + `Found ${baseTriggerable.length} nodes to trigger for market ${marketIndex}` + ); + } + + const triggerWithMaker: NodeToTriggerWithMakers[] = []; + + for (const nodeObj of baseTriggerable) { + const order = nodeObj.node.order; + + const makers = dlob.getBestMakers({ + marketIndex, + marketType, + direction: isVariant(order.direction, 'long') + ? PositionDirection.SHORT + : PositionDirection.LONG, + slot, + oraclePriceData, + numMakers: NUM_MAKERS, + }); + triggerWithMaker.push({ + ...nodeObj, + makers, + }); + } + + return triggerWithMaker; + } + protected async tryFill() { const startTime = Date.now(); let ran = false; @@ -2325,7 +2410,7 @@ export class FillerBot extends TxThreaded implements Bot { // 1) get all fillable nodes let fillableNodes: Array = []; - let triggerableNodes: Array = []; + let triggerableNodes: Array = []; for (const market of this.driftClient.getPerpMarketAccounts()) { try { const { nodesToFill, nodesToTrigger } = this.getPerpNodesForMarket( diff --git a/src/experimental-bots/filler-common/dlobBuilder.ts b/src/experimental-bots/filler-common/dlobBuilder.ts index 73e3703c..585aa20f 100644 --- a/src/experimental-bots/filler-common/dlobBuilder.ts +++ b/src/experimental-bots/filler-common/dlobBuilder.ts @@ -10,7 +10,6 @@ import { UserAccount, isVariant, decodeUser, - NodeToTrigger, Wallet, PhoenixSubscriber, BN, @@ -27,6 +26,8 @@ import { PositionDirection, UserStatus, isUserProtectedMaker, + OraclePriceData, + StateAccount, } from '@drift-labs/sdk'; import { Connection, PublicKey } from '@solana/web3.js'; import dotenv from 'dotenv'; @@ -37,6 +38,7 @@ import { SerializedNodeToFill, SerializedNodeToTrigger, NodeToFillWithContext, + NodeToTriggerWithMakers, } from './types'; import { getDriftClientFromArgs, @@ -47,6 +49,7 @@ import { initializeSpotFulfillmentAccounts, sleepMs } from '../../utils'; import { LRUCache } from 'lru-cache'; const EXPIRE_ORDER_BUFFER_SEC = 30; // add an extra 30 seconds before trying to expire orders (want to avoid 6252 error due to clock drift) +const NUM_MAKERS = 3; // number of makers to consider for triggerable nodes const logPrefix = '[DLOBBuilder]'; class DLOBBuilder { @@ -259,14 +262,14 @@ class DLOBBuilder { public getNodesToTriggerAndNodesToFill(): [ NodeToFillWithContext[], - NodeToTrigger[], + NodeToTriggerWithMakers[], ] { const dlob = this.build(); const nodesToFill: NodeToFillWithContext[] = []; - const nodesToTrigger: NodeToTrigger[] = []; + const nodesToTrigger: NodeToTriggerWithMakers[] = []; for (const marketIndex of this.marketIndexes) { let market; - let oraclePriceData; + let oraclePriceData: OraclePriceData; let fallbackAsk: BN | undefined = undefined; let fallbackBid: BN | undefined = undefined; let fallbackAskSource: FallbackLiquiditySource | undefined = undefined; @@ -342,13 +345,16 @@ class DLOBBuilder { stateAccount, market ); - const nodesToTriggerForMarket = dlob.findNodesToTrigger( - marketIndex, - slot, - oraclePriceData.price, - this.marketType, - stateAccount - ); + const nodesToTriggerForMarket: NodeToTriggerWithMakers[] = + this.findTriggerableNodesWithMakers( + dlob, + marketIndex, + slot, + oraclePriceData, + this.marketType, + stateAccount + ); + nodesToFill.push( ...nodesToFillForMarket.map((node) => { return { ...node, fallbackAskSource, fallbackBidSource }; @@ -359,6 +365,46 @@ class DLOBBuilder { return [nodesToFill, nodesToTrigger]; } + public findTriggerableNodesWithMakers( + dlob: DLOB, + marketIndex: number, + slot: number, + oraclePriceData: OraclePriceData, + marketType: MarketType, + stateAccount: StateAccount + ): NodeToTriggerWithMakers[] { + const baseTriggerable = dlob.findNodesToTrigger( + marketIndex, + slot, + oraclePriceData.price, + marketType, + stateAccount + ); + + const triggerWithMaker: NodeToTriggerWithMakers[] = []; + + for (const nodeObj of baseTriggerable) { + const order = nodeObj.node.order; + + const makers = dlob.getBestMakers({ + marketIndex, + marketType, + direction: isVariant(order.direction, 'long') + ? PositionDirection.SHORT + : PositionDirection.LONG, + slot, + oraclePriceData, + numMakers: NUM_MAKERS, + }); + triggerWithMaker.push({ + ...nodeObj, + makers, + }); + } + + return triggerWithMaker; + } + public serializeNodesToFill( nodesToFill: NodeToFillWithContext[] ): SerializedNodeToFill[] { @@ -392,7 +438,7 @@ class DLOBBuilder { } public serializeNodesToTrigger( - nodesToTrigger: NodeToTrigger[] + nodesToTrigger: NodeToTriggerWithMakers[] ): SerializedNodeToTrigger[] { return nodesToTrigger .map((node) => { @@ -400,7 +446,7 @@ class DLOBBuilder { if (!buffer) { return undefined; } - return serializeNodeToTrigger(node, buffer); + return serializeNodeToTrigger(node, buffer, node.makers); }) .filter((node): node is SerializedNodeToTrigger => node !== undefined); } diff --git a/src/experimental-bots/filler-common/types.ts b/src/experimental-bots/filler-common/types.ts index 3e658db6..28ed1fb1 100644 --- a/src/experimental-bots/filler-common/types.ts +++ b/src/experimental-bots/filler-common/types.ts @@ -7,6 +7,8 @@ import { SpotBalanceType, NodeToFill, DLOBNode, + NodeToTrigger, + PublicKey, } from '@drift-labs/sdk'; export type SerializedUserAccount = { @@ -95,6 +97,7 @@ export type SerializedPerpPosition = { export type SerializedNodeToTrigger = { node: SerializedTriggerOrderNode; + makers: string[]; }; export type SerializedTriggerOrderNode = { @@ -104,6 +107,8 @@ export type SerializedTriggerOrderNode = { sortValue: string; haveFilled: boolean; haveTrigger: boolean; + isSwift: boolean; + isUserProtectedMaker: boolean; }; export type SerializedNodeToFill = { @@ -143,3 +148,7 @@ export type NodeToFillWithBuffer = { makerNodes: DLOBNode[]; authority?: string; }; + +export type NodeToTriggerWithMakers = NodeToTrigger & { + makers: PublicKey[]; +}; diff --git a/src/experimental-bots/filler-common/utils.ts b/src/experimental-bots/filler-common/utils.ts index b62f926c..1017dab3 100644 --- a/src/experimental-bots/filler-common/utils.ts +++ b/src/experimental-bots/filler-common/utils.ts @@ -3,7 +3,6 @@ import { UserAccount, SpotPosition, PerpPosition, - NodeToTrigger, TriggerOrderNode, DLOBNode, OrderNode, @@ -39,6 +38,7 @@ import { SerializedDLOBNode, NodeToFillWithBuffer, NodeToFillWithContext, + NodeToTriggerWithMakers, } from './types'; import { ChildProcess, fork } from 'child_process'; import { logger } from '../../logger'; @@ -213,11 +213,13 @@ const deserializePerpPosition = ( }; export const serializeNodeToTrigger = ( - node: NodeToTrigger, - userAccountData: Buffer + node: NodeToTriggerWithMakers, + userAccountData: Buffer, + makers: PublicKey[] ): SerializedNodeToTrigger => { return { node: serializeTriggerOrderNode(node.node, userAccountData), + makers: makers.map((maker) => maker.toString()), }; }; @@ -232,6 +234,37 @@ const serializeTriggerOrderNode = ( sortValue: node.sortValue.toString('hex'), haveFilled: node.haveFilled, haveTrigger: node.haveTrigger, + isSwift: node.isSwift, + isUserProtectedMaker: node.isUserProtectedMaker, + }; +}; + +export const deserializeNodeToTriggerWithMakers = ( + serializedNode: SerializedNodeToTrigger +): NodeToTriggerWithMakers => { + return { + node: deserializeTriggerOrderNode(serializedNode.node), + makers: serializedNode.makers.map((m) => new PublicKey(m)), + }; +}; + +const deserializeTriggerOrderNode = ( + serializedNode: SerializedTriggerOrderNode +): TriggerOrderNode => { + const order = deserializeOrder(serializedNode.order); + return { + order, + userAccount: serializedNode.userAccount, + sortValue: new BN(serializedNode.sortValue, 'hex'), + haveFilled: serializedNode.haveFilled, + haveTrigger: serializedNode.haveTrigger, + isUserProtectedMaker: serializedNode.isUserProtectedMaker, + isSwift: serializedNode.isSwift, + isVammNode: () => false, + isBaseFilled: () => false, + getSortValue: () => new BN(0), + getLabel: () => '', + getPrice: () => new BN(0), }; }; @@ -322,7 +355,7 @@ export const deserializeNodeToFill = ( return node; }; -const deserializeDLOBNode = (node: SerializedDLOBNode): DLOBNode => { +export const deserializeDLOBNode = (node: SerializedDLOBNode): DLOBNode => { const order = deserializeOrder(node.order); switch (node.type) { case 'TakingLimitOrderNode': diff --git a/src/experimental-bots/filler/fillerMultithreaded.ts b/src/experimental-bots/filler/fillerMultithreaded.ts index 99fe9a78..3985c1c5 100644 --- a/src/experimental-bots/filler/fillerMultithreaded.ts +++ b/src/experimental-bots/filler/fillerMultithreaded.ts @@ -20,6 +20,7 @@ import { MakerInfo, MarketType, NodeToFill, + NodeToTrigger, PerpMarkets, PriorityFeeSubscriberMap, QUOTE_PRECISION, @@ -51,6 +52,7 @@ import { NodeToFillWithBuffer, SerializedNodeToTrigger, SerializedNodeToFill, + NodeToTriggerWithMakers, } from '../filler-common/types'; import { assert } from 'console'; import { @@ -73,9 +75,9 @@ import { import { spawnChild, deserializeNodeToFill, - deserializeOrder, getPriorityFeeInstruction, isTsRuntime, + deserializeNodeToTriggerWithMakers, } from '../filler-common/utils'; import { CounterValue, @@ -1085,7 +1087,7 @@ export class FillerMultithreaded { } private async getPythIxsFromNode( - node: NodeToFillWithBuffer | SerializedNodeToTrigger, + node: NodeToFillWithBuffer | NodeToTrigger | SerializedNodeToTrigger, precedingIxs: TransactionInstruction[] = [], isSwift = false ): Promise { @@ -1223,8 +1225,12 @@ export class FillerMultithreaded { this.throttledNodes.set(signature, Date.now()); } - protected removeTriggeringNodes(node: SerializedNodeToTrigger) { - this.triggeringNodes.delete(getNodeToTriggerSignature(node)); + protected removeTriggeringNodes(node: NodeToTriggerWithMakers) { + const nodeSignature = getOrderSignature( + node.node.order.orderId, + node.node.userAccount + ); + this.triggeringNodes.delete(nodeSignature); } protected pruneThrottledNode() { @@ -1358,9 +1364,41 @@ export class FillerMultithreaded { return true; } - async executeTriggerablePerpNodes(nodesToTrigger: SerializedNodeToTrigger[]) { - const user = this.driftClient.getUser(this.subaccount); - for (const nodeToTrigger of nodesToTrigger) { + protected async getUserAccountAndSlotFromMap( + key: string + ): Promise> { + const user = await this.userMap!.mustGetWithSlot( + key, + this.driftClient.userAccountSubscriptionConfig + ); + return { + data: user.data.getUserAccount(), + slot: user.slot, + }; + } + + protected async executeTriggerablePerpNodes( + nodesToTrigger: SerializedNodeToTrigger[] + ) { + const triggerableNodes = nodesToTrigger.map((node) => + deserializeNodeToTriggerWithMakers(node) + ); + const buildForBundle = this.shouldBuildForBundle(); + const subaccountUser = this.driftClient.getUser(this.subaccount); + + for (const nodeToTrigger of triggerableNodes) { + nodeToTrigger.node.haveTrigger = true; + const user = await this.getUserAccountAndSlotFromMap( + nodeToTrigger.node.userAccount.toString() + ); + logger.info( + `multithreaded: Processing node: account=${nodeToTrigger.node.userAccount.toString()}, slot=${ + user.slot + }, orderId=${nodeToTrigger.node.order.orderId.toString()}, numMakers=${ + nodeToTrigger.makers?.length ?? 0 + }` + ); + let ixs = [ ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000, @@ -1377,7 +1415,6 @@ export class FillerMultithreaded { }) ) * this.driftClient.txSender.getSuggestedPriorityFeeMultiplier() ); - const buildForBundle = this.shouldBuildForBundle(); if (buildForBundle) { ixs.push(this.bundleSender!.getTipIx()); @@ -1389,66 +1426,80 @@ export class FillerMultithreaded { ); } - nodeToTrigger.node.haveTrigger = true; - // @ts-ignore - const buffer = Buffer.from(nodeToTrigger.node.userAccountData.data); - // @ts-ignore - const userAccount = decodeUser(buffer); - - logger.debug( - `${logPrefix} trying to trigger (account: ${ - nodeToTrigger.node.userAccount - }, order ${nodeToTrigger.node.order.orderId.toString()}` - ); - let removeLastIxPostSim = this.revertOnFailure; if (this.pythPriceSubscriber) { - const pythIxs = await this.getPythIxsFromNode( - nodeToTrigger, - ixs, - false - ); + const pythIxs = await this.getPythIxsFromNode(nodeToTrigger, ixs); ixs.push(...pythIxs); - // do not strip the last ix if we have pyth ixs removeLastIxPostSim = false; } - const nodeSignature = getNodeToTriggerSignature(nodeToTrigger); - if (this.seenTriggerableOrders.has(nodeSignature)) { - if (!this.pythPriceSubscriber) { - // no pyth subscriber, tx will be empty - return; - } - logger.info( - `${logPrefix} already triggered order (account: ${ - nodeToTrigger.node.userAccount - }, order ${nodeToTrigger.node.order.orderId.toString()}. - Just going to pull oracles` - ); - } else { + const nodeSignature = getOrderSignature( + nodeToTrigger.node.order.orderId, + nodeToTrigger.node.userAccount + ); + if (!this.seenTriggerableOrders.has(nodeSignature)) { this.seenTriggerableOrders.add(nodeSignature); this.triggeringNodes.set(nodeSignature, Date.now()); ixs.push( await this.driftClient.getTriggerOrderIx( new PublicKey(nodeToTrigger.node.userAccount), - userAccount, - deserializeOrder(nodeToTrigger.node.order), - user.userAccountPublicKey + user.data, + nodeToTrigger.node.order ) ); + const makerInfos = await Promise.all( + nodeToTrigger.makers.map(async (maker) => { + const { data } = await this.getUserAccountAndSlotFromMap( + maker.toString() + ); + const makerUserAccount = data; + const makerAuthority = makerUserAccount.authority; + const makerStats = getUserStatsAccountPublicKey( + this.driftClient.program.programId, + makerAuthority + ); + return { + maker, + makerStats, + makerUserAccount, + }; + }) + ); + + const fillIx = await this.driftClient.getFillPerpOrderIx( + new PublicKey(nodeToTrigger.node.userAccount), + user.data, + nodeToTrigger.node.order, + makerInfos, + undefined, + this.subaccount + ); + ixs.push(fillIx); + if (this.revertOnFailure) { - console.log(user.userAccountPublicKey.toString()); ixs.push( - await this.driftClient.getRevertFillIx(user.userAccountPublicKey) + await this.driftClient.getRevertFillIx( + subaccountUser.userAccountPublicKey + ) ); } + } else { + if (!this.pythPriceSubscriber) { + logger.info(`No pyth price subscriber, skipping`); + return; + } + logger.info( + `Already triggered order ${nodeSignature}, only pulling oracles` + ); } const txSize = getSizeOfTransaction(ixs, true, this.lookupTableAccounts); if (txSize > PACKET_DATA_SIZE) { - logger.warn(`tx too large, removing pyth ixs.`); + logger.warn( + `Transaction too large (${txSize}), removing pyth instructions` + ); ixs = removePythIxs(ixs); } @@ -1466,28 +1517,28 @@ export class FillerMultithreaded { type: 'trigger', simError: simResult.simError !== null, ...metricAttrFromUserAccount( - user.userAccountPublicKey, - user.getUserAccount() + subaccountUser.userAccountPublicKey, + subaccountUser.getUserAccount() ), }); this.estTxCuHistogram?.record(simResult.cuEstimate, { type: 'trigger', simError: simResult.simError !== null, ...metricAttrFromUserAccount( - user.userAccountPublicKey, - user.getUserAccount() + subaccountUser.userAccountPublicKey, + subaccountUser.getUserAccount() ), }); logger.info( - `executeTriggerablePerpNodesForMarket (${nodeSignature}) estimated CUs: ${simResult.cuEstimate}, finalIxCount: ${ixs.length}). revertTx: ${this.revertOnFailure}}, buildForBundle: ${buildForBundle}` + `Simulation complete - CUs: ${simResult.cuEstimate}, ixCount: ${ixs.length}, revertTx: ${this.revertOnFailure}, buildForBundle: ${buildForBundle}` ); if (simResult.simError) { logger.error( - `executeTriggerablePerpNodesForMarket simError: (simError: ${JSON.stringify( - simResult.simError - )})` + `Simulation error: ${JSON.stringify(simResult.simError)}, ${ + simResult.simTxLogs + }` ); } else { if (this.hasEnoughSolToFill) { @@ -1500,7 +1551,7 @@ export class FillerMultithreaded { .sendTransaction(simResult.tx) .then((txSig) => { logger.info( - `${logPrefix} Triggered user (account: ${nodeToTrigger.node.userAccount.toString()}) order: ${nodeToTrigger.node.order.orderId.toString()} Tx: ${ + `Triggered successfully - user: ${nodeToTrigger.node.userAccount.toString()}, order: ${nodeToTrigger.node.order.orderId.toString()}, tx: ${ txSig.txSig }` ); @@ -1516,9 +1567,6 @@ export class FillerMultithreaded { 'Transaction was not confirmed' ) ) { - logger.error( - `Error (${errorCode}) triggering order for user (account: ${nodeToTrigger.node.userAccount.toString()}) order: ${nodeToTrigger.node.order.orderId.toString()}` - ); logger.error(error); } }) @@ -1527,17 +1575,15 @@ export class FillerMultithreaded { }); } } else { - logger.info( - `Not enough SOL to fill, skipping executeTriggerablePerpNodes` - ); + logger.info(`Insufficient SOL balance to fill`); } } } this.attemptedTriggersCounter?.add( nodesToTrigger.length, metricAttrFromUserAccount( - user.userAccountPublicKey, - user.getUserAccount() + subaccountUser.userAccountPublicKey, + subaccountUser.getUserAccount() ) ); } @@ -2576,7 +2622,20 @@ export class FillerMultithreaded { this.driftClient.program.programId, takerUserAccount!.authority ).toString(); - const referrerInfo = await this.referrerMap.mustGet(authority); + + // Add try-catch around referrer lookup + let referrerInfo: ReferrerInfo | undefined; + try { + // Existing referrer lookup code + if (nodeToFill.node.userAccount) { + referrerInfo = await this.referrerMap?.mustGet( + nodeToFill.node.userAccount + ); + } + } catch (e) { + logger.warn(`Failed to get referrer info: ${e}`); + referrerInfo = undefined; + } return Promise.resolve({ makerInfos,