From 81f433af8af3b0db984981669e7ac03d1dcfc290 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 4 Dec 2025 11:52:20 +0000 Subject: [PATCH 1/3] feat(pm-notifier): deeplink logs --- .../src/monitor-polymarket/common.ts | 64 ++++++++++++++++++- packages/toolkit/src/http/httpClient.ts | 4 +- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/common.ts b/packages/monitor-v2/src/monitor-polymarket/common.ts index e9685d5ede..6ce7d533fe 100644 --- a/packages/monitor-v2/src/monitor-polymarket/common.ts +++ b/packages/monitor-v2/src/monitor-polymarket/common.ts @@ -1,6 +1,6 @@ import { getRetryProvider, paginatedEventQuery as umaPaginatedEventQuery } from "@uma/common"; import { createHttpClient } from "@uma/toolkit"; -import { AxiosError, AxiosInstance } from "axios"; +import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios"; export const paginatedEventQuery = umaPaginatedEventQuery; import type { Provider } from "@ethersproject/abstract-provider"; @@ -75,10 +75,12 @@ export interface MonitoringParams { fillEventsLookbackSeconds: number; fillEventsProposalGapSeconds: number; httpClient: ReturnType; + aiDeeplinkHttpClient: ReturnType; orderBookBatchSize: number; ooV2Addresses: string[]; ooV1Addresses: string[]; aiConfig?: AIConfig; + aiDeeplinkTimeout: number; } interface PolymarketMarketGraphql { question: string; @@ -712,16 +714,22 @@ export async function fetchLatestAIDeepLink( if (!params.aiConfig) { return { deeplink: undefined }; } + const startTime = Date.now(); try { const questionId = calculatePolymarketQuestionID(proposal.ancillaryData); - const response = await params.httpClient.get(params.aiConfig.apiUrl, { + const requestConfig: AxiosRequestConfig = { params: { limit: 50, search: proposal.proposalHash, last_page: false, project_id: params.aiConfig.projectId, }, - }); + }; + + requestConfig.timeout = params.aiDeeplinkTimeout; + + const response = await params.aiDeeplinkHttpClient.get(params.aiConfig.apiUrl, requestConfig); + const duration = Date.now() - startTime; const result = response.data?.elements?.find( (element) => element.data.input.timing?.expiration_timestamp === proposal.proposalExpirationTimestamp.toNumber() @@ -739,20 +747,51 @@ export async function fetchLatestAIDeepLink( status: response.status, statusText: response.statusText, }, + durationMs: duration, notificationPath: "otb-monitoring", }); return { deeplink: undefined }; } + logger.debug({ + at: "PolymarketMonitor", + message: "Successfully fetched AI deeplink", + proposalHash: proposal.proposalHash, + durationMs: duration, + }); + return { deeplink: `${params.aiConfig.resultsBaseUrl}/${result.id}`, }; } catch (error) { + const duration = Date.now() - startTime; + const axiosError = error as AxiosError; + logger.debug({ at: "PolymarketMonitor", message: "Failed to fetch AI deeplink", err: error instanceof Error ? error.message : String(error), proposalHash: proposal.proposalHash, + durationMs: duration, + errorDetails: { + code: axiosError?.code, + response: axiosError?.response + ? { + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + headers: axiosError.response?.headers, + } + : undefined, + request: axiosError?.config + ? { + url: axiosError.config?.url, + method: axiosError.config?.method, + timeout: axiosError.config?.timeout, + baseURL: axiosError.config?.baseURL, + } + : undefined, + isTimeout: axiosError?.code === "ECONNABORTED" || (error instanceof Error && error.message?.includes("timeout")), + }, }); return { deeplink: undefined }; } @@ -902,6 +941,7 @@ export const initMonitoringParams = async ( const minTimeBetweenRequests = env.MIN_TIME_BETWEEN_REQUESTS ? Number(env.MIN_TIME_BETWEEN_REQUESTS) : 200; const httpTimeout = env.HTTP_TIMEOUT ? Number(env.HTTP_TIMEOUT) : 10_000; + const aiDeeplinkTimeout = env.AI_DEEPLINK_TIMEOUT ? Number(env.AI_DEEPLINK_TIMEOUT) : 10_000; const shouldResetTimeout = env.SHOULD_RESET_TIMEOUT !== "false"; @@ -924,6 +964,22 @@ export const initMonitoringParams = async ( }, }); + const aiDeeplinkHttpClient = createHttpClient({ + axios: { timeout: aiDeeplinkTimeout }, + rateLimit: { maxConcurrent: null, minTime: 0 }, // No rate limiting - unlimited concurrency + retry: { + retries: retryAttempts, + baseDelayMs: retryDelayMs, + shouldResetTimeout, + onRetry: (retryCount, err, config) => { + logger.debug({ + at: "PolymarketMonitor", + message: `ai-deeplink-retry attempt #${retryCount} for ${config?.url} after ${err.code}:${err.message}`, + }); + }, + }, + }); + const ooV2Addresses = parseEnvList(env, "OOV2_ADDRESSES", [await getAddress("OptimisticOracleV2", chainId)]); const ooV1Addresses = parseEnvList(env, "OOV1_ADDRESSES", [await getAddress("OptimisticOracle", chainId)]); @@ -947,10 +1003,12 @@ export const initMonitoringParams = async ( fillEventsLookbackSeconds, fillEventsProposalGapSeconds, httpClient, + aiDeeplinkHttpClient, orderBookBatchSize, ooV2Addresses, ooV1Addresses, aiConfig, + aiDeeplinkTimeout, }; }; diff --git a/packages/toolkit/src/http/httpClient.ts b/packages/toolkit/src/http/httpClient.ts index 0542c0f504..1055e134c9 100644 --- a/packages/toolkit/src/http/httpClient.ts +++ b/packages/toolkit/src/http/httpClient.ts @@ -3,8 +3,8 @@ import Bottleneck from "bottleneck"; import axiosRetry, { IAxiosRetryConfig } from "axios-retry"; export interface RateLimitOptions { - /** Max requests running in parallel (default = 5) */ - maxConcurrent?: number; + /** Max requests running in parallel (default = 5). Set to null for unlimited concurrency. */ + maxConcurrent?: number | null; /** Minimum gap in ms between jobs (default = 200 → ≈5 req/s) */ minTime?: number; } From 2026e006b5f5b02d228f5435c3523430421bbdde Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 4 Dec 2025 11:59:34 +0000 Subject: [PATCH 2/3] fix: maxConcurrent null setting --- .../src/monitor-polymarket/common.ts | 35 ++++++++++++------- packages/toolkit/src/http/httpClient.ts | 11 +++--- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/common.ts b/packages/monitor-v2/src/monitor-polymarket/common.ts index 6ce7d533fe..0fdb6ff394 100644 --- a/packages/monitor-v2/src/monitor-polymarket/common.ts +++ b/packages/monitor-v2/src/monitor-polymarket/common.ts @@ -728,7 +728,10 @@ export async function fetchLatestAIDeepLink( requestConfig.timeout = params.aiDeeplinkTimeout; - const response = await params.aiDeeplinkHttpClient.get(params.aiConfig.apiUrl, requestConfig); + const response = await params.aiDeeplinkHttpClient.get( + params.aiConfig.apiUrl, + requestConfig + ); const duration = Date.now() - startTime; const result = response.data?.elements?.find( @@ -777,20 +780,21 @@ export async function fetchLatestAIDeepLink( code: axiosError?.code, response: axiosError?.response ? { - status: axiosError.response?.status, - statusText: axiosError.response?.statusText, - headers: axiosError.response?.headers, - } + status: axiosError.response?.status, + statusText: axiosError.response?.statusText, + headers: axiosError.response?.headers, + } : undefined, request: axiosError?.config ? { - url: axiosError.config?.url, - method: axiosError.config?.method, - timeout: axiosError.config?.timeout, - baseURL: axiosError.config?.baseURL, - } + url: axiosError.config?.url, + method: axiosError.config?.method, + timeout: axiosError.config?.timeout, + baseURL: axiosError.config?.baseURL, + } : undefined, - isTimeout: axiosError?.code === "ECONNABORTED" || (error instanceof Error && error.message?.includes("timeout")), + isTimeout: + axiosError?.code === "ECONNABORTED" || (error instanceof Error && error.message?.includes("timeout")), }, }); return { deeplink: undefined }; @@ -964,17 +968,22 @@ export const initMonitoringParams = async ( }, }); + // Create a separate HTTP client for AI deeplink requests with unlimited concurrency + // This prevents AI deeplink requests from being queued behind other rate-limited requests const aiDeeplinkHttpClient = createHttpClient({ axios: { timeout: aiDeeplinkTimeout }, rateLimit: { maxConcurrent: null, minTime: 0 }, // No rate limiting - unlimited concurrency retry: { retries: retryAttempts, baseDelayMs: retryDelayMs, - shouldResetTimeout, + shouldResetTimeout: false, // Don't reset timeout on retries - keep total time bounded by single timeout + retry delays onRetry: (retryCount, err, config) => { logger.debug({ at: "PolymarketMonitor", - message: `ai-deeplink-retry attempt #${retryCount} for ${config?.url} after ${err.code}:${err.message}`, + message: `ai-deeplink-retry attempt #${retryCount} for ${config?.url}`, + error: err.code || err.message, + retryCount, + timeout: config?.timeout, }); }, }, diff --git a/packages/toolkit/src/http/httpClient.ts b/packages/toolkit/src/http/httpClient.ts index 1055e134c9..4f2d25e600 100644 --- a/packages/toolkit/src/http/httpClient.ts +++ b/packages/toolkit/src/http/httpClient.ts @@ -47,15 +47,18 @@ export interface HttpClientOptions { * @returns An Axios instance */ export function createHttpClient(opts: HttpClientOptions = {}): AxiosInstance { - const { maxConcurrent = 5, minTime = 200 } = opts.rateLimit ?? {}; - const limiter = new Bottleneck({ maxConcurrent, minTime }); - const instance = axios.create({ timeout: 10_000, // default timeout of 10 seconds ...opts.axios, }); - instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg)); + // Only use Bottleneck if maxConcurrent is not null (null means unlimited, skip rate limiting entirely) + const maxConcurrent = opts.rateLimit?.maxConcurrent ?? 5; + if (maxConcurrent !== null) { + const minTime = opts.rateLimit?.minTime ?? 200; + const limiter = new Bottleneck({ maxConcurrent, minTime }); + instance.interceptors.request.use((cfg) => limiter.schedule(async () => cfg)); + } const { retries = 3, retryCondition, onRetry, baseDelayMs = 100, maxJitterMs = 1000, maxDelayMs = 10_000 } = opts.retry ?? {}; From 7b4854027462f1a71d153ffe9f2f9133b7680894 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 8 Dec 2025 12:28:53 +0000 Subject: [PATCH 3/3] feat: optimise order fill event fetching --- .../MonitorProposalsOrderBook.ts | 46 ++++++++-- .../src/monitor-polymarket/common.ts | 92 ++++++++++++++----- packages/monitor-v2/test/PolymarketMonitor.ts | 60 ++++++++++++ 3 files changed, 167 insertions(+), 31 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts index 429bd29984..05902ba9e4 100644 --- a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts +++ b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts @@ -34,6 +34,8 @@ import { PolymarketMarketGraphqlProcessed, isInitialConfirmationLogged, fetchLatestAIDeepLink, + OrderFilledEventWithTrade, + fetchOrderFilledEvents, } from "./common"; import * as common from "./common"; @@ -47,6 +49,12 @@ function getThresholds() { } const blocksPerSecond = POLYGON_BLOCKS_PER_HOUR / 3_600; +type ProposalProcessingContext = { + currentBlock?: number; + lookbackBlocks?: number; + gapBlocks?: number; + orderFilledEvents?: OrderFilledEventWithTrade[]; +}; function outcomeIndexes( isSportsMarket: boolean, @@ -96,14 +104,16 @@ export async function processProposal( markets: PolymarketMarketGraphqlProcessed[], orderbooks: Record, params: MonitoringParams, - logger: typeof Logger + logger: typeof Logger, + context?: ProposalProcessingContext ): Promise { const thresholds = getThresholds(); const isSportsRequest = proposal.requester === params.ctfSportsOracleAddress; - const currentBlock = await params.provider.getBlockNumber(); - const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); - const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); + const currentBlock = context?.currentBlock ?? (await params.provider.getBlockNumber()); + const lookbackBlocks = + context?.lookbackBlocks ?? Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); + const gapBlocks = context?.gapBlocks ?? Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); const proposalGapStartBlock = Number(proposal.proposalBlockNumber) + gapBlocks; const checkMarket = async (market: PolymarketMarketGraphqlProcessed): Promise => { @@ -122,7 +132,10 @@ export async function processProposal( const buyingLoserSide = books[outcome.loser].bids.find((b) => b.price > thresholds.bids); const fromBlock = Math.max(proposalGapStartBlock, currentBlock - lookbackBlocks); - const fills = await getOrderFilledEvents(params, market.clobTokenIds, fromBlock); + const fills = await getOrderFilledEvents(params, market.clobTokenIds, fromBlock, { + cachedEvents: context?.orderFilledEvents, + toBlock: currentBlock, + }); const soldWinner = fills[outcome.winner].filter((f) => f.type === "sell" && f.price < thresholds.asks); const boughtLoser = fills[outcome.loser].filter((f) => f.type === "buy" && f.price > thresholds.bids); @@ -313,10 +326,31 @@ export async function monitorTransactionsProposedOrderBook( activeBundles = survivingBundles; } + if (!activeBundles.length) { + console.log("All proposals have been checked!"); + return; + } + + const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); + const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); + const currentBlock = await params.provider.getBlockNumber(); + + const fromBlocks = activeBundles.map(({ proposal }) => + Math.max(Number(proposal.proposalBlockNumber) + gapBlocks, currentBlock - lookbackBlocks) + ); + const earliestFromBlock = Math.min(...fromBlocks); + const orderFilledEventsPromise = fetchOrderFilledEvents(params, earliestFromBlock, currentBlock); + await Promise.all( activeBundles.map(async ({ proposal, markets }) => { try { - const alerted = await processProposal(proposal, markets, orderbookMap, params, logger); + const sharedOrderFilledEvents = await orderFilledEventsPromise; + const alerted = await processProposal(proposal, markets, orderbookMap, params, logger, { + currentBlock, + lookbackBlocks, + gapBlocks, + orderFilledEvents: sharedOrderFilledEvents, + }); if (alerted) await persistNotified(proposal, logger); } catch (err) { await logErrorAndPersist(proposal, err as Error); diff --git a/packages/monitor-v2/src/monitor-polymarket/common.ts b/packages/monitor-v2/src/monitor-polymarket/common.ts index 0fdb6ff394..4b192fcc23 100644 --- a/packages/monitor-v2/src/monitor-polymarket/common.ts +++ b/packages/monitor-v2/src/monitor-polymarket/common.ts @@ -107,6 +107,13 @@ export interface PolymarketTradeInformation { timestamp: number; } +export interface OrderFilledEventWithTrade { + blockNumber: number; + makerAssetId: string; + takerAssetId: string; + trade: PolymarketTradeInformation; +} + export interface PolymarketOrderBook { market: string; asset_id: string; @@ -378,9 +385,10 @@ export const getPolymarketMarketInformation = async ( const getTradeInfoFromOrderFilledEvent = async ( provider: Provider, - event: any + event: any, + blockTimestamp?: number ): Promise => { - const blockTimestamp = (await provider.getBlock(event.blockNumber)).timestamp; + const timestamp = blockTimestamp ?? (await provider.getBlock(event.blockNumber)).timestamp; const isBuy = event.args.makerAssetId.toString() === "0"; const numerator = (isBuy ? event.args.makerAmountFilled : event.args.takerAmountFilled).mul(1000); const denominator = isBuy ? event.args.takerAmountFilled : event.args.makerAmountFilled; @@ -388,30 +396,28 @@ const getTradeInfoFromOrderFilledEvent = async ( return { price, type: isBuy ? "buy" : "sell", - timestamp: blockTimestamp, + timestamp, // Convert to decimal value with 2 decimals amount: (isBuy ? event.args.takerAmountFilled : event.args.makerAmountFilled).div(10_000).toNumber() / 100, }; }; -export const getOrderFilledEvents = async ( +export const fetchOrderFilledEvents = async ( params: MonitoringParams, - clobTokenIds: [string, string], - startBlockNumber: number -): Promise<[PolymarketTradeInformation[], PolymarketTradeInformation[]]> => { + startBlockNumber: number, + endBlockNumber?: number +): Promise => { const ctfExchange = new ethers.Contract( params.ctfExchangeAddress, require("./abi/ctfExchange.json"), params.provider ); - const currentBlockNumber = await params.provider.getBlockNumber(); - const maxBlockLookBack = params.maxBlockLookBack; - + const toBlock = endBlockNumber ?? (await params.provider.getBlockNumber()); const searchConfig = { fromBlock: startBlockNumber, - toBlock: currentBlockNumber, - maxBlockLookBack, + toBlock, + maxBlockLookBack: params.maxBlockLookBack, }; const events: Event[] = await paginatedEventQuery( @@ -422,25 +428,61 @@ export const getOrderFilledEvents = async ( queryFilterSafe ); - const outcomeTokenOne = await Promise.all( - events - .filter((event) => { - return [event?.args?.takerAssetId.toString(), event?.args?.makerAssetId.toString()].includes(clobTokenIds[0]); - }) - .map((event) => getTradeInfoFromOrderFilledEvent(params.provider, event)) - ); + const blockTimestamps = new Map(); + const getTimestamp = async (blockNumber: number): Promise => { + if (!blockTimestamps.has(blockNumber)) { + blockTimestamps.set(blockNumber, (await params.provider.getBlock(blockNumber)).timestamp); + } + return blockTimestamps.get(blockNumber)!; + }; - const outcomeTokenTwo = await Promise.all( - events - .filter((event) => { - return [event?.args?.takerAssetId.toString(), event?.args?.makerAssetId.toString()].includes(clobTokenIds[1]); - }) - .map((event) => getTradeInfoFromOrderFilledEvent(params.provider, event)) + return Promise.all( + events.map(async (event) => { + const blockTimestamp = await getTimestamp(event.blockNumber); + return { + blockNumber: event.blockNumber, + makerAssetId: event?.args?.makerAssetId.toString(), + takerAssetId: event?.args?.takerAssetId.toString(), + trade: await getTradeInfoFromOrderFilledEvent(params.provider, event, blockTimestamp), + }; + }) ); +}; + +export const filterOrderFilledEvents = ( + orderFilledEvents: OrderFilledEventWithTrade[], + clobTokenIds: [string, string], + startBlockNumber: number +): [PolymarketTradeInformation[], PolymarketTradeInformation[]] => { + const [tokenOne, tokenTwo] = clobTokenIds; + const eventsWithinWindow = orderFilledEvents.filter((event) => event.blockNumber >= startBlockNumber); + + const outcomeTokenOne = eventsWithinWindow + .filter((event) => [event.takerAssetId, event.makerAssetId].includes(tokenOne)) + .map((event) => event.trade); + + const outcomeTokenTwo = eventsWithinWindow + .filter((event) => [event.takerAssetId, event.makerAssetId].includes(tokenTwo)) + .map((event) => event.trade); return [outcomeTokenOne, outcomeTokenTwo]; }; +export const getOrderFilledEvents = async ( + params: MonitoringParams, + clobTokenIds: [string, string], + startBlockNumber: number, + opts?: { + toBlock?: number; + cachedEvents?: OrderFilledEventWithTrade[]; + } +): Promise<[PolymarketTradeInformation[], PolymarketTradeInformation[]]> => { + const orderFilledEvents = + opts?.cachedEvents ?? (await fetchOrderFilledEvents(params, startBlockNumber, opts?.toBlock)); + + return filterOrderFilledEvents(orderFilledEvents, clobTokenIds, startBlockNumber); +}; + export const calculatePolymarketQuestionID = (ancillaryData: string): string => { return ethers.utils.keccak256(ancillaryData); }; diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index 2141f58c37..bd8b594817 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -44,6 +44,7 @@ describe("PolymarketNotifier", function () { let deployer: Signer; let votingToken: VotingTokenEthers; let getNotifiedProposalsStub: sinon.SinonStub; + let fetchOrderFilledEventsStub: sinon.SinonStub; const identifier = formatBytes32String("TEST_IDENTIFIER"); const ancillaryData = toUtf8Bytes(`q:"Really hard question, maybe 100, maybe 90?"`); @@ -167,6 +168,7 @@ describe("PolymarketNotifier", function () { sandbox.stub(commonModule, "isProposalNotified").resolves(false); sandbox.stub(commonModule, "fetchLatestAIDeepLink").resolves({ deeplink: undefined }); + fetchOrderFilledEventsStub = sandbox.stub(commonModule, "fetchOrderFilledEvents").resolves([]); // Fund staker and stake tokens. const TEN_MILLION = ethers.utils.parseEther("10000000"); @@ -1143,6 +1145,64 @@ describe("PolymarketNotifier", function () { }); }); + it("fetches OrderFilled events once using the earliest fromBlock across proposals", async function () { + const params = await createMonitoringParams(); + params.fillEventsLookbackSeconds = 7_200; + + const currentBlock = 2_000; + const providerStub = { getBlockNumber: sandbox.stub().resolves(currentBlock) } as unknown as Provider; + params.provider = providerStub; + + const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * (commonModule.POLYGON_BLOCKS_PER_HOUR / 3_600)); + const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * (commonModule.POLYGON_BLOCKS_PER_HOUR / 3_600)); + + const makeProposal = async (proposalBlockNumber: number, hash: string): Promise => ({ + proposalHash: hash, + requester: params.additionalRequesters[0], + proposer: await deployer.getAddress(), + identifier, + proposedPrice: ONE, + requestTimestamp: ethers.BigNumber.from(Date.now()), + proposalBlockNumber, + ancillaryData: ethers.utils.hexlify(ancillaryData), + requestHash: `0xrequest${hash}`, + requestLogIndex: 0, + proposalTimestamp: ethers.BigNumber.from(Date.now()), + proposalExpirationTimestamp: ethers.BigNumber.from(Date.now() + 3_600), + proposalLogIndex: 0, + }); + + const proposalA = await makeProposal(1_600, "0xpropA"); + const proposalB = await makeProposal(1_000, "0xpropB"); + + const expectedEarliestFromBlock = Math.min( + Math.max(proposalA.proposalBlockNumber + gapBlocks, currentBlock - lookbackBlocks), + Math.max(proposalB.proposalBlockNumber + gapBlocks, currentBlock - lookbackBlocks) + ); + + fetchOrderFilledEventsStub.resetHistory(); + + sandbox + .stub(commonModule, "getPolymarketProposedPriceRequestsOO") + .callsFake(async (_params, version) => (version === "v2" ? [proposalA, proposalB] : [])); + sandbox.stub(commonModule, "getPolymarketMarketInformation").resolves(marketInfo); + sandbox.stub(commonModule, "getPolymarketOrderBooks").resolves(asBooksRecord(emptyOrders)); + const getOrderFilledEventsSpy = sandbox.spy(commonModule, "getOrderFilledEvents"); + + sandbox.stub(commonModule, "isInitialConfirmationLogged").resolves(true); + sandbox.stub(commonModule, "markInitialConfirmationLogged").resolves(); + + const logger = createNewLogger([new SpyTransport({}, { spy: sinon.spy() })]); + await monitorTransactionsProposedOrderBook(logger, params); + + sinon.assert.calledOnce(fetchOrderFilledEventsStub); + sinon.assert.calledWithExactly(fetchOrderFilledEventsStub, params, expectedEarliestFromBlock, currentBlock); + assert.equal(getOrderFilledEventsSpy.callCount, 2, "fills are filtered per proposal"); + const cachedEventsArgs = getOrderFilledEventsSpy.getCalls().map((call) => call.args[3]?.cachedEvents); + assert.isDefined(cachedEventsArgs[0], "cached events are forwarded into per-market filters"); + assert.strictEqual(cachedEventsArgs[0], cachedEventsArgs[1], "shared event cache is reused across proposals"); + }); + it("getOrderFilledEvents uses the fillEventsLookbackSeconds", async function () { const currentBlock = 100_000; const fillEventsLookbackSeconds = 3_600; // 1 hour