From 8895cc0126268f2b99aa006d05ef2371332ecc08 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 26 Jan 2026 14:51:17 +0000 Subject: [PATCH 1/9] feat(monitor-v2): add interactive local runner script for Polymarket notifier --- .../src/monitor-polymarket/run-local.sh | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100755 packages/monitor-v2/src/monitor-polymarket/run-local.sh diff --git a/packages/monitor-v2/src/monitor-polymarket/run-local.sh b/packages/monitor-v2/src/monitor-polymarket/run-local.sh new file mode 100755 index 0000000000..e972757be1 --- /dev/null +++ b/packages/monitor-v2/src/monitor-polymarket/run-local.sh @@ -0,0 +1,245 @@ +#!/bin/bash +# +# Polymarket Notifier Local Runner +# Interactive script to run the Polymarket notifier in one-shot mode +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +warn() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +prompt() { + echo -e "${GREEN}[?]${NC} $1" +} + +# Header +echo "" +echo "==============================================" +echo " Polymarket Notifier - Local Runner" +echo "==============================================" +echo "" +info "This script will guide you through running the Polymarket notifier locally in one-shot mode." +echo "" + +# Step 1: Determine UMA Protocol path +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# The script is in packages/monitor-v2/src/monitor-polymarket/ +UMA_PROTOCOL_DEFAULT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +echo "==============================================" +echo " Step 1: Repository Paths" +echo "==============================================" +echo "" + +info "Detected UMA Protocol path: $UMA_PROTOCOL_DEFAULT" +prompt "Press Enter to use this path, or enter a different path:" +read -r UMA_PROTOCOL_INPUT +UMA_PROTOCOL="${UMA_PROTOCOL_INPUT:-$UMA_PROTOCOL_DEFAULT}" + +# Validate UMA Protocol path +if [[ ! -d "$UMA_PROTOCOL/packages/monitor-v2" ]]; then + error "Invalid UMA Protocol path: $UMA_PROTOCOL" + error "Could not find packages/monitor-v2 directory" + exit 1 +fi +success "UMA Protocol path: $UMA_PROTOCOL" +echo "" + +# Check for bot-configs repository +prompt "Enter the path to your bot-configs repository (required for .env generation):" +echo " If you don't have it, clone it first:" +echo " git clone https://github.com/UMAprotocol/bot-configs.git" +echo "" +read -r UMA_BOT_CONFIGS + +if [[ -z "$UMA_BOT_CONFIGS" ]]; then + error "bot-configs path is required" + exit 1 +fi + +# Expand ~ if present +UMA_BOT_CONFIGS="${UMA_BOT_CONFIGS/#\~/$HOME}" + +if [[ ! -d "$UMA_BOT_CONFIGS" ]]; then + error "bot-configs directory not found: $UMA_BOT_CONFIGS" + exit 1 +fi + +if [[ ! -f "$UMA_BOT_CONFIGS/scripts/print-env-file.js" ]]; then + error "Invalid bot-configs repository: scripts/print-env-file.js not found" + exit 1 +fi +success "bot-configs path: $UMA_BOT_CONFIGS" +echo "" + +# Export paths +export UMA_PROTOCOL +export UMA_BOT_CONFIGS + +# Step 2: Install dependencies in bot-configs +echo "==============================================" +echo " Step 2: Install Dependencies (bot-configs)" +echo "==============================================" +echo "" + +prompt "Do you need to install/update dependencies in bot-configs? (y/N)" +read -r INSTALL_BOT_CONFIGS_DEPS + +if [[ "$INSTALL_BOT_CONFIGS_DEPS" =~ ^[Yy]$ ]]; then + info "Installing dependencies in bot-configs..." + cd "$UMA_BOT_CONFIGS" + yarn install + success "bot-configs dependencies installed" +else + info "Skipping bot-configs dependency installation" +fi +echo "" + +# Step 3: Generate .env file +echo "==============================================" +echo " Step 3: Generate .env File" +echo "==============================================" +echo "" + +ENV_FILE="$UMA_PROTOCOL/packages/monitor-v2/src/monitor-polymarket/.env.local" + +if [[ -f "$ENV_FILE" ]]; then + warn "Existing .env.local found: $ENV_FILE" + prompt "Do you want to regenerate it? (y/N)" + read -r REGENERATE_ENV + GENERATE_ENV=false + if [[ "$REGENERATE_ENV" =~ ^[Yy]$ ]]; then + GENERATE_ENV=true + fi +else + GENERATE_ENV=true +fi + +if [[ "$GENERATE_ENV" == "true" ]]; then + info "Generating .env.local file..." + + node "$UMA_BOT_CONFIGS/scripts/print-env-file.js" \ + "$UMA_BOT_CONFIGS/serverless-bots/uma-config-5m.json" polymarket-polygon-notifier \ + | grep -Ev '^(SLACK_CONFIG|PAGER_DUTY_V2_CONFIG|DISCORD_CONFIG|DISCORD_TICKET_CONFIG|REDIS_URL|NODE_OPTIONS)=' \ + > "$ENV_FILE" + + # Add local-specific settings + printf 'LOCAL_NO_DATASTORE=true\nNODE_OPTIONS=--max-old-space-size=16000\n' >> "$ENV_FILE" + + # Set POLLING_DELAY=0 for one-shot mode + if grep -q '^POLLING_DELAY=' "$ENV_FILE"; then + sed -i 's/^POLLING_DELAY=.*/POLLING_DELAY=0/' "$ENV_FILE" + else + echo 'POLLING_DELAY=0' >> "$ENV_FILE" + fi + + success ".env.local generated at: $ENV_FILE" +else + info "Using existing .env.local file" + # Ensure POLLING_DELAY=0 for one-shot mode + if grep -q '^POLLING_DELAY=' "$ENV_FILE"; then + sed -i 's/^POLLING_DELAY=.*/POLLING_DELAY=0/' "$ENV_FILE" + else + echo 'POLLING_DELAY=0' >> "$ENV_FILE" + fi +fi +echo "" + +# Step 4: Build monitor-v2 +echo "==============================================" +echo " Step 4: Build monitor-v2 Package" +echo "==============================================" +echo "" + +MONITOR_V2_DIR="$UMA_PROTOCOL/packages/monitor-v2" +DIST_FILE="$MONITOR_V2_DIR/dist/monitor-polymarket/index.js" + +if [[ -f "$DIST_FILE" ]]; then + info "Existing build found: $DIST_FILE" + prompt "Do you want to rebuild? (y/N)" + read -r REBUILD + BUILD_NEEDED=false + if [[ "$REBUILD" =~ ^[Yy]$ ]]; then + BUILD_NEEDED=true + fi +else + BUILD_NEEDED=true +fi + +if [[ "$BUILD_NEEDED" == "true" ]]; then + info "Building monitor-v2 package..." + cd "$MONITOR_V2_DIR" + + prompt "Do you need to install dependencies first? (y/N)" + read -r INSTALL_DEPS + if [[ "$INSTALL_DEPS" =~ ^[Yy]$ ]]; then + yarn install + fi + + yarn build + success "monitor-v2 built successfully" +else + info "Skipping build, using existing dist" +fi +echo "" + +# Step 5: Run the notifier +echo "==============================================" +echo " Step 5: Run Polymarket Notifier" +echo "==============================================" +echo "" + +info "Configuration summary:" +echo " - UMA Protocol: $UMA_PROTOCOL" +echo " - bot-configs: $UMA_BOT_CONFIGS" +echo " - ENV file: $ENV_FILE" +echo " - Mode: one-shot (POLLING_DELAY=0)" +echo "" + +prompt "Ready to run the Polymarket notifier. Continue? (Y/n)" +read -r RUN_CONFIRM + +if [[ "$RUN_CONFIRM" =~ ^[Nn]$ ]]; then + info "Aborted. You can run manually with:" + echo "" + echo " cd $MONITOR_V2_DIR" + echo " DOTENV_CONFIG_PATH=$ENV_FILE DOTENV_CONFIG_OVERRIDE=true \\" + echo " node -r dotenv/config ./dist/monitor-polymarket/index.js" + echo "" + exit 0 +fi + +info "Starting Polymarket Notifier..." +echo "" +echo "----------------------------------------------" +echo "" + +cd "$MONITOR_V2_DIR" +DOTENV_CONFIG_PATH="$ENV_FILE" DOTENV_CONFIG_OVERRIDE=true \ + node -r dotenv/config ./dist/monitor-polymarket/index.js + +echo "" +echo "----------------------------------------------" +success "Polymarket Notifier finished" From 75549e6dd21bc345d2e6579fd943ef08534d379d Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 28 Jan 2026 10:12:37 +0000 Subject: [PATCH 2/9] refactor(monitor-v2): simplify AI deeplink generation in Polymarket monitor Replace API-based deeplink fetching with deterministic URL calculation. The new format is: {baseUrl}/{proposalHash}?index={eventIndex} - Remove fetchLatestAIDeepLink() and related async HTTP request logic - Add simple generateAIDeepLink() function - Remove AIConfig interface, use aiResultsBaseUrl string directly - Remove aiDeeplinkHttpClient and aiDeeplinkTimeout from MonitoringParams - Require AI_RESULTS_BASE_URL environment variable --- .../MonitorProposalsOrderBook.ts | 31 +-- .../src/monitor-polymarket/common.ts | 192 ++---------------- packages/monitor-v2/test/PolymarketMonitor.ts | 12 +- 3 files changed, 23 insertions(+), 212 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts index bd45659b7c..1848f0b530 100644 --- a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts +++ b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts @@ -12,6 +12,7 @@ import { decodeMultipleQueryPriceAtIndex, decodeMultipleValuesQuery, fetchOrderFilledEventsBounded, + generateAIDeepLink, getNotifiedProposals, getOrderFilledEvents, getPolymarketMarketInformation, @@ -37,7 +38,6 @@ import { OptimisticPriceRequest, PolymarketMarketGraphqlProcessed, isInitialConfirmationLogged, - fetchLatestAIDeepLink, } from "./common"; import * as common from "./common"; @@ -239,7 +239,7 @@ export async function monitorTransactionsProposedOrderBook( const tokenIds = new Set(); const logErrorAndPersist = async (proposal: OptimisticPriceRequest, err: Error) => { - const { deeplink: aiDeeplink } = await fetchLatestAIDeepLink(proposal, params, logger); + const aiDeeplink = generateAIDeepLink(proposal.proposalHash, proposal.proposalLogIndex, params.aiResultsBaseUrl); await logFailedMarketProposalVerification(logger, params.chainId, proposal, err as Error, aiDeeplink); await persistNotified(proposal, logger); }; @@ -353,7 +353,7 @@ export async function monitorTransactionsProposedOrderBook( // Fetch OrderFilled events with bounded memory to prevent V8 crashes // Only collect sells for winner tokens and buys for loser tokens const thresholds = getThresholds(); - const boundedTradesMapPromise = fetchOrderFilledEventsBounded( + const boundedTradesMap = await fetchOrderFilledEventsBounded( params, earliestFromBlock, currentBlock, @@ -363,27 +363,12 @@ export async function monitorTransactionsProposedOrderBook( params.maxTradesPerToken ); - // Fetch all AI deeplinks in advance + // Generate AI deeplinks synchronously for each proposal const aiDeeplinksMap = new Map(); - await Promise.all( - activeBundles.map(async ({ proposal }) => { - try { - const { deeplink } = await fetchLatestAIDeepLink(proposal, params, logger); - if (deeplink) { - aiDeeplinksMap.set(getProposalKeyToStore(proposal), deeplink); - } - } catch (err) { - logger.warn({ - at: "PolymarketMonitor", - message: "Failed to fetch AI deeplink for proposal", - proposalHash: proposal.proposalHash, - error: err, - }); - } - }) - ); - - const boundedTradesMap = await boundedTradesMapPromise; + for (const { proposal } of activeBundles) { + const deeplink = generateAIDeepLink(proposal.proposalHash, proposal.proposalLogIndex, params.aiResultsBaseUrl); + aiDeeplinksMap.set(getProposalKeyToStore(proposal), deeplink); + } await BluebirdPromise.map( activeBundles, diff --git a/packages/monitor-v2/src/monitor-polymarket/common.ts b/packages/monitor-v2/src/monitor-polymarket/common.ts index 7a1f8fa325..dca45886b9 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, AxiosRequestConfig } from "axios"; +import { AxiosError, AxiosInstance } from "axios"; export const paginatedEventQuery = umaPaginatedEventQuery; import type { Provider } from "@ethersproject/abstract-provider"; @@ -94,12 +94,10 @@ export interface MonitoringParams { fillEventsLookbackSeconds: number; fillEventsProposalGapSeconds: number; httpClient: ReturnType; - aiDeeplinkHttpClient: ReturnType; orderBookBatchSize: number; ooV2Addresses: string[]; ooV1Addresses: string[]; - aiConfig?: AIConfig; - aiDeeplinkTimeout: number; + aiResultsBaseUrl: string; proposalProcessingConcurrency: number; marketProcessingConcurrency: number; paginatedEventQueryConcurrency: number; @@ -778,125 +776,15 @@ export function decodeMultipleValuesQuery(decodedAncillaryData: string): Multipl return json; } -export interface UMAAIRetry { - id: string; - question_id: string; - data: { - input: { - timing?: { - expiration_time?: string; - }; - }; - }; -} - -export interface UMAAIRetriesLatestResponse { - elements: UMAAIRetry[]; - next_cursor: string | null; - has_more: boolean; - total_count: number; - total_pages: number; -} -interface AIRetryLookupResult { - deeplink?: string; -} - -export async function fetchLatestAIDeepLink( - proposal: OptimisticPriceRequest, - params: MonitoringParams, - logger: typeof Logger -): Promise { - if (!params.aiConfig) { - return { deeplink: undefined }; - } - const startTime = Date.now(); - try { - const questionId = calculatePolymarketQuestionID(proposal.ancillaryData); - 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) => { - const expirationTime = element.data.input.timing?.expiration_time; - if (!expirationTime) return false; - const expirationTimestamp = Math.floor(new Date(expirationTime).getTime() / 1000); - return expirationTimestamp === proposal.proposalExpirationTimestamp.toNumber(); - }); - - if (!result) { - logger.debug({ - at: "PolymarketMonitor", - message: "No AI deeplink found for proposal", - proposalHash: proposal.proposalHash, - expirationTimestamp: proposal.proposalExpirationTimestamp.toNumber(), - questionId: questionId, - response: { - data: response.data, - 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 }; - } +/** + * Generates a deterministic AI deeplink URL for a proposal. + * @param proposalHash - The transaction hash of the proposal + * @param eventIndex - The log index of the proposal event + * @param baseUrl - The base URL for AI results + * @returns The AI deeplink URL + */ +export function generateAIDeepLink(proposalHash: string, eventIndex: number, baseUrl: string): string { + return `${baseUrl}/${proposalHash}?index=${eventIndex}`; } export const getProposalKeyToStore = (market: StoredNotifiedProposal | OptimisticPriceRequest): string => { @@ -981,18 +869,6 @@ export const parseEnvList = (env: NodeJS.ProcessEnv, key: string, defaultValue: return output; }; -export const parseEnvJson = (env: NodeJS.ProcessEnv, key: string, defaultValue: T): T => { - const rawValue = env[key]; - if (!rawValue) return defaultValue; - return JSON.parse(rawValue); -}; - -export interface AIConfig { - projectId: string; - apiUrl: string; - resultsBaseUrl: string; -} - export const initMonitoringParams = async ( env: NodeJS.ProcessEnv, logger: typeof Logger @@ -1009,14 +885,8 @@ export const initMonitoringParams = async ( if (!env.POLYMARKET_API_KEY) throw new Error("POLYMARKET_API_KEY must be defined in env"); const polymarketApiKey = env.POLYMARKET_API_KEY; - const rawAiConfig = parseEnvJson(env, "AI_CONFIG", { - projectId: "", - apiUrl: "", - resultsBaseUrl: "", - }); - - // Only set aiConfig if all required fields are present - const aiConfig = rawAiConfig.apiUrl && rawAiConfig.projectId && rawAiConfig.resultsBaseUrl ? rawAiConfig : undefined; + if (!env.AI_RESULTS_BASE_URL) throw new Error("AI_RESULTS_BASE_URL must be defined in env"); + const aiResultsBaseUrl = env.AI_RESULTS_BASE_URL; // Creating provider will check for other chainId specific env variables. const provider = getRetryProvider(chainId) as Provider; @@ -1060,15 +930,7 @@ export const initMonitoringParams = async ( const maxConcurrentRequests = env.MAX_CONCURRENT_REQUESTS ? Number(env.MAX_CONCURRENT_REQUESTS) : 5; const minTimeBetweenRequests = env.MIN_TIME_BETWEEN_REQUESTS ? Number(env.MIN_TIME_BETWEEN_REQUESTS) : 200; - const aiDeeplinkMaxConcurrentRequests = env.AI_DEEPLINK_MAX_CONCURRENT_REQUESTS - ? Number(env.AI_DEEPLINK_MAX_CONCURRENT_REQUESTS) - : 10; - const aiDeeplinkMinTimeBetweenRequests = env.AI_DEEPLINK_MIN_TIME_BETWEEN_REQUESTS - ? Number(env.AI_DEEPLINK_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"; @@ -1091,30 +953,6 @@ export const initMonitoringParams = async ( }, }); - // Create a separate HTTP client for AI deeplink requests with configurable rate limiting - // This prevents AI deeplink requests from being queued behind other rate-limited requests - const aiDeeplinkHttpClient = createHttpClient({ - axios: { timeout: aiDeeplinkTimeout }, - rateLimit: { - maxConcurrent: aiDeeplinkMaxConcurrentRequests, - minTime: aiDeeplinkMinTimeBetweenRequests, - }, - retry: { - retries: retryAttempts, - baseDelayMs: retryDelayMs, - 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}`, - error: err.code || err.message, - retryCount, - timeout: config?.timeout, - }); - }, - }, - }); - const ooV2Addresses = parseEnvList(env, "OOV2_ADDRESSES", [await getAddress("OptimisticOracleV2", chainId)]); const ooV1Addresses = parseEnvList(env, "OOV1_ADDRESSES", [await getAddress("OptimisticOracle", chainId)]); @@ -1138,12 +976,10 @@ export const initMonitoringParams = async ( fillEventsLookbackSeconds, fillEventsProposalGapSeconds, httpClient, - aiDeeplinkHttpClient, orderBookBatchSize, ooV2Addresses, ooV1Addresses, - aiConfig, - aiDeeplinkTimeout, + aiResultsBaseUrl, proposalProcessingConcurrency, marketProcessingConcurrency, paginatedEventQueryConcurrency, diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index 655af42362..1d7d2286f1 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -85,9 +85,7 @@ describe("PolymarketNotifier", function () { const ctfSportsOracleAddress = "0x1234"; const graphqlEndpoint = "endpoint"; const apiEndpoint = "endpoint"; - const aiApiUrl = "https://ai.example.com/api"; const aiResultsBaseUrl = "https://ai.example.com/results"; - const aiProjectId = "test-project"; return { ctfExchangeAddress, @@ -107,16 +105,10 @@ describe("PolymarketNotifier", function () { fillEventsLookbackSeconds: 0, fillEventsProposalGapSeconds: 300, httpClient: createHttpClient(), - aiDeeplinkHttpClient: createHttpClient(), orderBookBatchSize: 499, ooV2Addresses: [oov2.address], ooV1Addresses: [oo.address], - aiConfig: { - apiUrl: aiApiUrl, - resultsBaseUrl: aiResultsBaseUrl, - projectId: aiProjectId, - }, - aiDeeplinkTimeout: 5000, + aiResultsBaseUrl, proposalProcessingConcurrency: 5, marketProcessingConcurrency: 3, paginatedEventQueryConcurrency: 5, @@ -170,8 +162,6 @@ describe("PolymarketNotifier", function () { sandbox.stub(commonModule, "storeNotifiedProposals").callsFake(storeNotifiedProposalsMock); sandbox.stub(commonModule, "isProposalNotified").resolves(false); - sandbox.stub(commonModule, "fetchLatestAIDeepLink").resolves({ deeplink: undefined }); - // Fund staker and stake tokens. const TEN_MILLION = ethers.utils.parseEther("10000000"); await (await votingToken.addMinter(await deployer.getAddress())).wait(); From cac5776c83de0e686331893119232ffd83dfa267 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 28 Jan 2026 10:14:56 +0000 Subject: [PATCH 3/9] chore(monitor-v2): remove run-local.sh script --- .../src/monitor-polymarket/run-local.sh | 245 ------------------ 1 file changed, 245 deletions(-) delete mode 100755 packages/monitor-v2/src/monitor-polymarket/run-local.sh diff --git a/packages/monitor-v2/src/monitor-polymarket/run-local.sh b/packages/monitor-v2/src/monitor-polymarket/run-local.sh deleted file mode 100755 index e972757be1..0000000000 --- a/packages/monitor-v2/src/monitor-polymarket/run-local.sh +++ /dev/null @@ -1,245 +0,0 @@ -#!/bin/bash -# -# Polymarket Notifier Local Runner -# Interactive script to run the Polymarket notifier in one-shot mode -# - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Helper functions -info() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -warn() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -prompt() { - echo -e "${GREEN}[?]${NC} $1" -} - -# Header -echo "" -echo "==============================================" -echo " Polymarket Notifier - Local Runner" -echo "==============================================" -echo "" -info "This script will guide you through running the Polymarket notifier locally in one-shot mode." -echo "" - -# Step 1: Determine UMA Protocol path -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# The script is in packages/monitor-v2/src/monitor-polymarket/ -UMA_PROTOCOL_DEFAULT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" - -echo "==============================================" -echo " Step 1: Repository Paths" -echo "==============================================" -echo "" - -info "Detected UMA Protocol path: $UMA_PROTOCOL_DEFAULT" -prompt "Press Enter to use this path, or enter a different path:" -read -r UMA_PROTOCOL_INPUT -UMA_PROTOCOL="${UMA_PROTOCOL_INPUT:-$UMA_PROTOCOL_DEFAULT}" - -# Validate UMA Protocol path -if [[ ! -d "$UMA_PROTOCOL/packages/monitor-v2" ]]; then - error "Invalid UMA Protocol path: $UMA_PROTOCOL" - error "Could not find packages/monitor-v2 directory" - exit 1 -fi -success "UMA Protocol path: $UMA_PROTOCOL" -echo "" - -# Check for bot-configs repository -prompt "Enter the path to your bot-configs repository (required for .env generation):" -echo " If you don't have it, clone it first:" -echo " git clone https://github.com/UMAprotocol/bot-configs.git" -echo "" -read -r UMA_BOT_CONFIGS - -if [[ -z "$UMA_BOT_CONFIGS" ]]; then - error "bot-configs path is required" - exit 1 -fi - -# Expand ~ if present -UMA_BOT_CONFIGS="${UMA_BOT_CONFIGS/#\~/$HOME}" - -if [[ ! -d "$UMA_BOT_CONFIGS" ]]; then - error "bot-configs directory not found: $UMA_BOT_CONFIGS" - exit 1 -fi - -if [[ ! -f "$UMA_BOT_CONFIGS/scripts/print-env-file.js" ]]; then - error "Invalid bot-configs repository: scripts/print-env-file.js not found" - exit 1 -fi -success "bot-configs path: $UMA_BOT_CONFIGS" -echo "" - -# Export paths -export UMA_PROTOCOL -export UMA_BOT_CONFIGS - -# Step 2: Install dependencies in bot-configs -echo "==============================================" -echo " Step 2: Install Dependencies (bot-configs)" -echo "==============================================" -echo "" - -prompt "Do you need to install/update dependencies in bot-configs? (y/N)" -read -r INSTALL_BOT_CONFIGS_DEPS - -if [[ "$INSTALL_BOT_CONFIGS_DEPS" =~ ^[Yy]$ ]]; then - info "Installing dependencies in bot-configs..." - cd "$UMA_BOT_CONFIGS" - yarn install - success "bot-configs dependencies installed" -else - info "Skipping bot-configs dependency installation" -fi -echo "" - -# Step 3: Generate .env file -echo "==============================================" -echo " Step 3: Generate .env File" -echo "==============================================" -echo "" - -ENV_FILE="$UMA_PROTOCOL/packages/monitor-v2/src/monitor-polymarket/.env.local" - -if [[ -f "$ENV_FILE" ]]; then - warn "Existing .env.local found: $ENV_FILE" - prompt "Do you want to regenerate it? (y/N)" - read -r REGENERATE_ENV - GENERATE_ENV=false - if [[ "$REGENERATE_ENV" =~ ^[Yy]$ ]]; then - GENERATE_ENV=true - fi -else - GENERATE_ENV=true -fi - -if [[ "$GENERATE_ENV" == "true" ]]; then - info "Generating .env.local file..." - - node "$UMA_BOT_CONFIGS/scripts/print-env-file.js" \ - "$UMA_BOT_CONFIGS/serverless-bots/uma-config-5m.json" polymarket-polygon-notifier \ - | grep -Ev '^(SLACK_CONFIG|PAGER_DUTY_V2_CONFIG|DISCORD_CONFIG|DISCORD_TICKET_CONFIG|REDIS_URL|NODE_OPTIONS)=' \ - > "$ENV_FILE" - - # Add local-specific settings - printf 'LOCAL_NO_DATASTORE=true\nNODE_OPTIONS=--max-old-space-size=16000\n' >> "$ENV_FILE" - - # Set POLLING_DELAY=0 for one-shot mode - if grep -q '^POLLING_DELAY=' "$ENV_FILE"; then - sed -i 's/^POLLING_DELAY=.*/POLLING_DELAY=0/' "$ENV_FILE" - else - echo 'POLLING_DELAY=0' >> "$ENV_FILE" - fi - - success ".env.local generated at: $ENV_FILE" -else - info "Using existing .env.local file" - # Ensure POLLING_DELAY=0 for one-shot mode - if grep -q '^POLLING_DELAY=' "$ENV_FILE"; then - sed -i 's/^POLLING_DELAY=.*/POLLING_DELAY=0/' "$ENV_FILE" - else - echo 'POLLING_DELAY=0' >> "$ENV_FILE" - fi -fi -echo "" - -# Step 4: Build monitor-v2 -echo "==============================================" -echo " Step 4: Build monitor-v2 Package" -echo "==============================================" -echo "" - -MONITOR_V2_DIR="$UMA_PROTOCOL/packages/monitor-v2" -DIST_FILE="$MONITOR_V2_DIR/dist/monitor-polymarket/index.js" - -if [[ -f "$DIST_FILE" ]]; then - info "Existing build found: $DIST_FILE" - prompt "Do you want to rebuild? (y/N)" - read -r REBUILD - BUILD_NEEDED=false - if [[ "$REBUILD" =~ ^[Yy]$ ]]; then - BUILD_NEEDED=true - fi -else - BUILD_NEEDED=true -fi - -if [[ "$BUILD_NEEDED" == "true" ]]; then - info "Building monitor-v2 package..." - cd "$MONITOR_V2_DIR" - - prompt "Do you need to install dependencies first? (y/N)" - read -r INSTALL_DEPS - if [[ "$INSTALL_DEPS" =~ ^[Yy]$ ]]; then - yarn install - fi - - yarn build - success "monitor-v2 built successfully" -else - info "Skipping build, using existing dist" -fi -echo "" - -# Step 5: Run the notifier -echo "==============================================" -echo " Step 5: Run Polymarket Notifier" -echo "==============================================" -echo "" - -info "Configuration summary:" -echo " - UMA Protocol: $UMA_PROTOCOL" -echo " - bot-configs: $UMA_BOT_CONFIGS" -echo " - ENV file: $ENV_FILE" -echo " - Mode: one-shot (POLLING_DELAY=0)" -echo "" - -prompt "Ready to run the Polymarket notifier. Continue? (Y/n)" -read -r RUN_CONFIRM - -if [[ "$RUN_CONFIRM" =~ ^[Nn]$ ]]; then - info "Aborted. You can run manually with:" - echo "" - echo " cd $MONITOR_V2_DIR" - echo " DOTENV_CONFIG_PATH=$ENV_FILE DOTENV_CONFIG_OVERRIDE=true \\" - echo " node -r dotenv/config ./dist/monitor-polymarket/index.js" - echo "" - exit 0 -fi - -info "Starting Polymarket Notifier..." -echo "" -echo "----------------------------------------------" -echo "" - -cd "$MONITOR_V2_DIR" -DOTENV_CONFIG_PATH="$ENV_FILE" DOTENV_CONFIG_OVERRIDE=true \ - node -r dotenv/config ./dist/monitor-polymarket/index.js - -echo "" -echo "----------------------------------------------" -success "Polymarket Notifier finished" From 65d6266970bbbcd6cfb9f271b9cdcd8c3716537a Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 28 Jan 2026 14:05:39 +0000 Subject: [PATCH 4/9] fix(monitor-v2): add per-proposal time filtering for fill events After aggregating fetchOrderFilledEventsBounded across all proposal tokens, FILL_EVENTS_LOOKBACK_SECONDS and FILL_EVENTS_PROPOSAL_GAP_SECONDS no longer applied per-proposal. The code computed per-proposal fromBlocks but only used the global minimum, with no subsequent per-proposal filtering. Pass a per-proposal fromTimestamp into processProposal and filter trades by timestamp >= fromTimestamp before checking price discrepancies. --- .../MonitorProposalsOrderBook.ts | 24 ++++++++++++++--- packages/monitor-v2/test/PolymarketMonitor.ts | 26 +++++++++++++------ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts index 1848f0b530..19c5e514d6 100644 --- a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts +++ b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts @@ -54,6 +54,7 @@ const blocksPerSecond = POLYGON_BLOCKS_PER_HOUR / 3_600; type ProposalProcessingContext = { boundedTradesMap: Map; aiDeeplink?: string; + fromTimestamp: number; }; function outcomeIndexes( @@ -128,8 +129,13 @@ export async function processProposal( const fills = getOrderFilledEvents(market.clobTokenIds, context.boundedTradesMap); - const soldWinner = fills[outcome.winner].filter((f) => isDiscrepantTrade(f, "winner", thresholds)); - const boughtLoser = fills[outcome.loser].filter((f) => isDiscrepantTrade(f, "loser", thresholds)); + // Filter trades by proposal-specific fromTimestamp and price thresholds + const soldWinner = fills[outcome.winner].filter( + (f) => f.timestamp >= context.fromTimestamp && isDiscrepantTrade(f, "winner", thresholds) + ); + const boughtLoser = fills[outcome.loser].filter( + (f) => f.timestamp >= context.fromTimestamp && isDiscrepantTrade(f, "loser", thresholds) + ); let alerted = false; @@ -320,12 +326,21 @@ export async function monitorTransactionsProposedOrderBook( const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); const currentBlock = await params.provider.getBlockNumber(); + const currentTimestamp = Math.floor(Date.now() / 1000); const fromBlocks = activeBundles.map(({ proposal }) => Math.max(Number(proposal.proposalBlockNumber) + gapBlocks, currentBlock - lookbackBlocks) ); const earliestFromBlock = Math.min(...fromBlocks); + // Pre-compute fromTimestamp for each proposal to enable per-proposal trade filtering + const fromTimestampsMap = new Map(); + for (let i = 0; i < activeBundles.length; i++) { + const proposalKey = getProposalKeyToStore(activeBundles[i].proposal); + const fromTimestamp = currentTimestamp - Math.round((currentBlock - fromBlocks[i]) / blocksPerSecond); + fromTimestampsMap.set(proposalKey, fromTimestamp); + } + // Pre-compute winner/loser for each market to enable targeted event filtering const winnerTokenIds = new Set(); const loserTokenIds = new Set(); @@ -374,10 +389,13 @@ export async function monitorTransactionsProposedOrderBook( activeBundles, async ({ proposal, markets }) => { try { - const aiDeeplink = aiDeeplinksMap.get(getProposalKeyToStore(proposal)); + const proposalKey = getProposalKeyToStore(proposal); + const aiDeeplink = aiDeeplinksMap.get(proposalKey); + const fromTimestamp = fromTimestampsMap.get(proposalKey)!; const alerted = await processProposal(proposal, markets, orderbookMap, params, logger, { boundedTradesMap, aiDeeplink, + fromTimestamp, }); if (alerted) await persistNotified(proposal, logger); } catch (err) { diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index 1d7d2286f1..4fe3ff6de7 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -41,6 +41,7 @@ describe("PolymarketNotifier", function () { let deployer: Signer; let votingToken: VotingTokenEthers; let getNotifiedProposalsStub: sinon.SinonStub; + let fetchBoundedStub: sinon.SinonStub; const identifier = formatBytes32String("TEST_IDENTIFIER"); const ancillaryData = toUtf8Bytes(`q:"Really hard question, maybe 100, maybe 90?"`); @@ -102,8 +103,8 @@ describe("PolymarketNotifier", function () { retryAttempts: 3, retryDelayMs: 1000, checkBeforeExpirationSeconds: Date.now() + 1000 * 60 * 60 * 24, - fillEventsLookbackSeconds: 0, - fillEventsProposalGapSeconds: 300, + fillEventsLookbackSeconds: 3600, // 1 hour lookback to ensure trades can be fetched + fillEventsProposalGapSeconds: 0, // No gap for tests - ensures trades aren't filtered by timestamp httpClient: createHttpClient(), orderBookBatchSize: 499, ooV2Addresses: [oov2.address], @@ -162,6 +163,10 @@ describe("PolymarketNotifier", function () { sandbox.stub(commonModule, "storeNotifiedProposals").callsFake(storeNotifiedProposalsMock); sandbox.stub(commonModule, "isProposalNotified").resolves(false); + // Mock fetchOrderFilledEventsBounded to avoid actual blockchain queries with fake addresses + // Tests that need to override this should call fetchBoundedStub.restore() first + fetchBoundedStub = sandbox.stub(commonModule, "fetchOrderFilledEventsBounded").resolves(new Map()); + // Fund staker and stake tokens. const TEN_MILLION = ethers.utils.parseEther("10000000"); await (await votingToken.addMinter(await deployer.getAddress())).wait(); @@ -421,13 +426,14 @@ describe("PolymarketNotifier", function () { }); it("It should notify if there are sell trades over the threshold", async function () { + const currentTimestamp = Math.floor(Date.now() / 1000); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [ { price: 0.9, type: "sell", amount: 100, - timestamp: 123, + timestamp: currentTimestamp, }, ], [], @@ -461,6 +467,7 @@ describe("PolymarketNotifier", function () { }); it("It should notify if there are buy trades over the threshold", async function () { + const currentTimestamp = Math.floor(Date.now() / 1000); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [], [ @@ -468,7 +475,7 @@ describe("PolymarketNotifier", function () { price: 0.1, type: "buy", amount: 100, - timestamp: 123, + timestamp: currentTimestamp, }, ], ]; @@ -732,6 +739,7 @@ describe("PolymarketNotifier", function () { it("It should not notify if already notified", async function () { sandbox.restore(); + const currentTimestamp = Math.floor(Date.now() / 1000); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [], [ @@ -739,7 +747,7 @@ describe("PolymarketNotifier", function () { price: 0.1, type: "buy", amount: 100, - timestamp: 123, + timestamp: currentTimestamp, }, ], ]; @@ -803,13 +811,14 @@ describe("PolymarketNotifier", function () { }); it("It should notify two times if there are buy trades over the threshold and it's a high volume market proposal", async function () { + const currentTimestamp = Math.floor(Date.now() / 1000); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [ { price: 0.9, type: "sell", amount: 100, - timestamp: 123, + timestamp: currentTimestamp, }, ], [], @@ -1120,9 +1129,10 @@ describe("PolymarketNotifier", function () { Math.max(proposalB.proposalBlockNumber + gapBlocks, currentBlock - lookbackBlocks) ); - // Stub fetchOrderFilledEventsBounded to return an empty map + // Restore the default stub and re-stub to verify call arguments const boundedTradesMap = new Map(); - const fetchBoundedStub = sandbox.stub(commonModule, "fetchOrderFilledEventsBounded").resolves(boundedTradesMap); + fetchBoundedStub.restore(); + fetchBoundedStub = sandbox.stub(commonModule, "fetchOrderFilledEventsBounded").resolves(boundedTradesMap); sandbox .stub(commonModule, "getPolymarketProposedPriceRequestsOO") From 26ca3699af93e8ff66c46546fac6ad97f4cbb8e9 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 28 Jan 2026 14:39:00 +0000 Subject: [PATCH 5/9] test(monitor-v2): add test proving per-proposal time filtering works Adds a test that specifically verifies FILL_EVENTS_PROPOSAL_GAP_SECONDS and FILL_EVENTS_LOOKBACK_SECONDS apply per-proposal: - Creates two proposals at different blocks (5000 and 9000) - Sets up a trade with a timestamp that falls between the two proposals' fromTimestamp thresholds - Verifies only the older proposal triggers an alert, while the newer proposal correctly filters out the trade --- packages/monitor-v2/test/PolymarketMonitor.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index 4fe3ff6de7..e30515c62e 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -1161,6 +1161,123 @@ describe("PolymarketNotifier", function () { assert.strictEqual(boundedMapArgs[0], boundedMapArgs[1], "shared bounded map is reused across proposals"); }); + it("filters trades by per-proposal fromTimestamp, not just the global earliest block", async function () { + // This test verifies that FILL_EVENTS_PROPOSAL_GAP_SECONDS applies per-proposal. + // Setup: Two proposals at different blocks, same market, one discrepant trade. + // The trade timestamp should pass the filter for the older proposal but fail for the newer one. + const params = await createMonitoringParams(); + params.fillEventsLookbackSeconds = 7_200; // 2 hours + params.fillEventsProposalGapSeconds = 1_800; // 30 minute gap + + const blocksPerSecond = commonModule.POLYGON_BLOCKS_PER_HOUR / 3_600; + const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); + + const currentBlock = 10_000; + const providerStub = ({ getBlockNumber: sandbox.stub().resolves(currentBlock) } as unknown) as Provider; + params.provider = providerStub; + + // Fix Date.now() to a known timestamp for predictable calculations + const fixedNow = 1700000000 * 1000; // milliseconds + const currentTimestamp = fixedNow / 1000; // seconds + sandbox.stub(Date, "now").returns(fixedNow); + + // Proposal A: created at block 5000 (older) + // fromBlock for A = max(5000 + gapBlocks, 10000 - lookbackBlocks) = 5000 + 900 = 5900 + // fromTimestamp for A = currentTimestamp - (currentBlock - 5900) / blocksPerSecond + // = currentTimestamp - (10000 - 5900) / 0.5 = currentTimestamp - 8200 + const proposalABlock = 5_000; + + // Proposal B: created at block 9000 (newer) + // fromBlock for B = max(9000 + gapBlocks, 10000 - lookbackBlocks) = 9000 + 900 = 9900 + // fromTimestamp for B = currentTimestamp - (currentBlock - 9900) / blocksPerSecond + // = currentTimestamp - (10000 - 9900) / 0.5 = currentTimestamp - 200 + const proposalBBlock = 9_000; + + const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); + // Use Math.max() same as production code + const fromBlockA = Math.max(proposalABlock + gapBlocks, currentBlock - lookbackBlocks); + const fromBlockB = Math.max(proposalBBlock + gapBlocks, currentBlock - lookbackBlocks); + const fromTimestampA = currentTimestamp - Math.round((currentBlock - fromBlockA) / blocksPerSecond); + const fromTimestampB = currentTimestamp - Math.round((currentBlock - fromBlockB) / blocksPerSecond); + + // Create a trade timestamp that is: + // - AFTER fromTimestampA (should trigger alert for proposal A) + // - BEFORE fromTimestampB (should NOT trigger alert for proposal B) + const tradeTimestamp = fromTimestampA + 100; // 100 seconds after A's threshold + assert.isTrue(tradeTimestamp >= fromTimestampA, "trade should pass filter for proposal A"); + assert.isTrue(tradeTimestamp < fromTimestampB, "trade should fail filter for proposal B"); + + 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(currentTimestamp), + proposalBlockNumber, + ancillaryData: ethers.utils.hexlify(ancillaryData), + requestHash: `0xrequest${hash}`, + requestLogIndex: 0, + proposalTimestamp: ethers.BigNumber.from(currentTimestamp), + proposalExpirationTimestamp: ethers.BigNumber.from(currentTimestamp + 3_600), + proposalLogIndex: 0, + }); + + const proposalA = await makeProposal(proposalABlock, "0xpropA"); + const proposalB = await makeProposal(proposalBBlock, "0xpropB"); + + // Both proposals use the same market (same clobTokenIds) + // Trade: selling winner at 0.9 (below threshold) - should be flagged as discrepant + const discrepantTrade: PolymarketTradeInformation = { + price: 0.9, + type: "sell", + amount: 100, + timestamp: tradeTimestamp, + }; + + // boundedTradesMap contains trades keyed by tokenId + const boundedTradesMap = new Map(); + boundedTradesMap.set(marketInfo[0].clobTokenIds[0], [discrepantTrade]); // winner token + boundedTradesMap.set(marketInfo[0].clobTokenIds[1], []); // loser token + + fetchBoundedStub.restore(); + fetchBoundedStub = sandbox.stub(commonModule, "fetchOrderFilledEventsBounded").resolves(boundedTradesMap); + + 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)); + sandbox.stub(commonModule, "isInitialConfirmationLogged").resolves(true); + sandbox.stub(commonModule, "markInitialConfirmationLogged").resolves(); + + const spy = sinon.spy(); + const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]); + await monitorTransactionsProposedOrderBook(spyLogger, params); + + // Should have exactly 1 alert (for proposal A only, not B) + // The alert is an error log with "Difference between proposed price and market signal!" + const discrepancyAlerts: sinon.SinonSpyCall[] = []; + for (let i = 0; i < spy.callCount; i++) { + if ( + spyLogLevel(spy, i) === "error" && + spy.getCall(i).lastArg?.message?.includes("Difference between proposed price and market signal!") + ) { + discrepancyAlerts.push(spy.getCall(i)); + } + } + + assert.equal( + discrepancyAlerts.length, + 1, + "Should have exactly 1 discrepancy alert (for older proposal A, not newer proposal B)" + ); + + // Verify the alert is for proposal A (check the mrkdwn contains the proposal hash) + const alertLog = discrepancyAlerts[0].lastArg; + assert.include(alertLog.mrkdwn, "0xpropA", "Alert should be for proposal A"); + }); + describe("getPolymarketProposedPriceRequestsOO Filtering", function () { it("should return only events that are close enough to expiration (current time > expirationTimestamp - checkBeforeExpirationSeconds)", async function () { const fakeRequester = "0x0000000000000000000000000000000000000000"; // Address 0 From 23345aa1cd92279cc54ea1ec2d66685e52e0a46f Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 28 Jan 2026 16:15:03 +0000 Subject: [PATCH 6/9] refactor(monitor-v2): replace Maps with augmented bundles for cleaner data flow Instead of maintaining separate fromTimestampsMap and aiDeeplinksMap, augment the bundles directly with per-proposal context (fromTimestamp, aiDeeplink, fromBlock). This: - Eliminates map lookups and duplicate key computations - Removes non-null assertions - Makes data flow more explicit - each bundle owns its context - Reduces code by ~10 lines Also makes aiDeeplink non-optional in ProposalProcessingContext since it's always computed, and fixes test timing issues with trade timestamps. --- .../MonitorProposalsOrderBook.ts | 41 +++++++------------ packages/monitor-v2/test/PolymarketMonitor.ts | 13 ++++-- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts index 19c5e514d6..136f238cf1 100644 --- a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts +++ b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts @@ -53,7 +53,7 @@ function getThresholds() { const blocksPerSecond = POLYGON_BLOCKS_PER_HOUR / 3_600; type ProposalProcessingContext = { boundedTradesMap: Map; - aiDeeplink?: string; + aiDeeplink: string; fromTimestamp: number; }; @@ -328,25 +328,22 @@ export async function monitorTransactionsProposedOrderBook( const currentBlock = await params.provider.getBlockNumber(); const currentTimestamp = Math.floor(Date.now() / 1000); - const fromBlocks = activeBundles.map(({ proposal }) => - Math.max(Number(proposal.proposalBlockNumber) + gapBlocks, currentBlock - lookbackBlocks) - ); - const earliestFromBlock = Math.min(...fromBlocks); - - // Pre-compute fromTimestamp for each proposal to enable per-proposal trade filtering - const fromTimestampsMap = new Map(); - for (let i = 0; i < activeBundles.length; i++) { - const proposalKey = getProposalKeyToStore(activeBundles[i].proposal); - const fromTimestamp = currentTimestamp - Math.round((currentBlock - fromBlocks[i]) / blocksPerSecond); - fromTimestampsMap.set(proposalKey, fromTimestamp); - } + // Augment bundles with per-proposal context (fromTimestamp, aiDeeplink) + const augmentedBundles = activeBundles.map(({ proposal, markets }) => { + const fromBlock = Math.max(Number(proposal.proposalBlockNumber) + gapBlocks, currentBlock - lookbackBlocks); + const fromTimestamp = currentTimestamp - Math.round((currentBlock - fromBlock) / blocksPerSecond); + const aiDeeplink = generateAIDeepLink(proposal.proposalHash, proposal.proposalLogIndex, params.aiResultsBaseUrl); + return { proposal, markets, fromBlock, fromTimestamp, aiDeeplink }; + }); + + const earliestFromBlock = Math.min(...augmentedBundles.map((b) => b.fromBlock)); // Pre-compute winner/loser for each market to enable targeted event filtering const winnerTokenIds = new Set(); const loserTokenIds = new Set(); await Promise.all( - activeBundles.map(async ({ proposal, markets }) => { + augmentedBundles.map(async ({ proposal, markets }) => { const isSportsRequest = proposal.requester === params.ctfSportsOracleAddress; await Promise.all( @@ -378,20 +375,10 @@ export async function monitorTransactionsProposedOrderBook( params.maxTradesPerToken ); - // Generate AI deeplinks synchronously for each proposal - const aiDeeplinksMap = new Map(); - for (const { proposal } of activeBundles) { - const deeplink = generateAIDeepLink(proposal.proposalHash, proposal.proposalLogIndex, params.aiResultsBaseUrl); - aiDeeplinksMap.set(getProposalKeyToStore(proposal), deeplink); - } - await BluebirdPromise.map( - activeBundles, - async ({ proposal, markets }) => { + augmentedBundles, + async ({ proposal, markets, fromTimestamp, aiDeeplink }) => { try { - const proposalKey = getProposalKeyToStore(proposal); - const aiDeeplink = aiDeeplinksMap.get(proposalKey); - const fromTimestamp = fromTimestampsMap.get(proposalKey)!; const alerted = await processProposal(proposal, markets, orderbookMap, params, logger, { boundedTradesMap, aiDeeplink, @@ -407,6 +394,6 @@ export async function monitorTransactionsProposedOrderBook( logger.debug({ at: "PolymarketMonitor", - message: `${activeBundles.length} proposals processed successfully!`, + message: `${augmentedBundles.length} proposals processed successfully!`, }); } diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index e30515c62e..cc11e8e5ad 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -426,7 +426,8 @@ describe("PolymarketNotifier", function () { }); it("It should notify if there are sell trades over the threshold", async function () { - const currentTimestamp = Math.floor(Date.now() / 1000); + // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter + const currentTimestamp = Math.floor(Date.now() / 1000) + 10; const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [ { @@ -467,7 +468,8 @@ describe("PolymarketNotifier", function () { }); it("It should notify if there are buy trades over the threshold", async function () { - const currentTimestamp = Math.floor(Date.now() / 1000); + // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter + const currentTimestamp = Math.floor(Date.now() / 1000) + 10; const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [], [ @@ -739,7 +741,8 @@ describe("PolymarketNotifier", function () { it("It should not notify if already notified", async function () { sandbox.restore(); - const currentTimestamp = Math.floor(Date.now() / 1000); + // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter + const currentTimestamp = Math.floor(Date.now() / 1000) + 10; const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [], [ @@ -811,7 +814,9 @@ describe("PolymarketNotifier", function () { }); it("It should notify two times if there are buy trades over the threshold and it's a high volume market proposal", async function () { - const currentTimestamp = Math.floor(Date.now() / 1000); + // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter + // (the main code computes fromTimestamp from Date.now() which runs after this) + const currentTimestamp = Math.floor(Date.now() / 1000) + 10; const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [ { From f15b74a57a9c3500e235da631635c22450375d24 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 29 Jan 2026 10:14:55 +0000 Subject: [PATCH 7/9] fix(monitor-v2): use block timestamp and clarify trade filter naming - Fetch currentTimestamp from provider.getBlock() instead of Date.now() to ensure consistency with block-derived fromBlock calculation - Rename fromTimestamp to tradeFilterFromTimestamp for clarity - Update test mocks to include getBlock stub --- .../MonitorProposalsOrderBook.ts | 20 +++++++++---------- packages/monitor-v2/test/PolymarketMonitor.ts | 17 +++++++++------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts index 136f238cf1..91190bcf58 100644 --- a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts +++ b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts @@ -54,7 +54,7 @@ const blocksPerSecond = POLYGON_BLOCKS_PER_HOUR / 3_600; type ProposalProcessingContext = { boundedTradesMap: Map; aiDeeplink: string; - fromTimestamp: number; + tradeFilterFromTimestamp: number; }; function outcomeIndexes( @@ -129,12 +129,12 @@ export async function processProposal( const fills = getOrderFilledEvents(market.clobTokenIds, context.boundedTradesMap); - // Filter trades by proposal-specific fromTimestamp and price thresholds + // Filter trades by proposal-specific tradeFilterFromTimestamp and price thresholds const soldWinner = fills[outcome.winner].filter( - (f) => f.timestamp >= context.fromTimestamp && isDiscrepantTrade(f, "winner", thresholds) + (f) => f.timestamp >= context.tradeFilterFromTimestamp && isDiscrepantTrade(f, "winner", thresholds) ); const boughtLoser = fills[outcome.loser].filter( - (f) => f.timestamp >= context.fromTimestamp && isDiscrepantTrade(f, "loser", thresholds) + (f) => f.timestamp >= context.tradeFilterFromTimestamp && isDiscrepantTrade(f, "loser", thresholds) ); let alerted = false; @@ -326,14 +326,14 @@ export async function monitorTransactionsProposedOrderBook( const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); const currentBlock = await params.provider.getBlockNumber(); - const currentTimestamp = Math.floor(Date.now() / 1000); + const currentTimestamp = (await params.provider.getBlock(currentBlock)).timestamp; - // Augment bundles with per-proposal context (fromTimestamp, aiDeeplink) + // Augment bundles with per-proposal context (tradeFilterFromTimestamp, aiDeeplink) const augmentedBundles = activeBundles.map(({ proposal, markets }) => { const fromBlock = Math.max(Number(proposal.proposalBlockNumber) + gapBlocks, currentBlock - lookbackBlocks); - const fromTimestamp = currentTimestamp - Math.round((currentBlock - fromBlock) / blocksPerSecond); + const tradeFilterFromTimestamp = currentTimestamp - Math.round((currentBlock - fromBlock) / blocksPerSecond); const aiDeeplink = generateAIDeepLink(proposal.proposalHash, proposal.proposalLogIndex, params.aiResultsBaseUrl); - return { proposal, markets, fromBlock, fromTimestamp, aiDeeplink }; + return { proposal, markets, fromBlock, tradeFilterFromTimestamp, aiDeeplink }; }); const earliestFromBlock = Math.min(...augmentedBundles.map((b) => b.fromBlock)); @@ -377,12 +377,12 @@ export async function monitorTransactionsProposedOrderBook( await BluebirdPromise.map( augmentedBundles, - async ({ proposal, markets, fromTimestamp, aiDeeplink }) => { + async ({ proposal, markets, tradeFilterFromTimestamp, aiDeeplink }) => { try { const alerted = await processProposal(proposal, markets, orderbookMap, params, logger, { boundedTradesMap, aiDeeplink, - fromTimestamp, + tradeFilterFromTimestamp, }); if (alerted) await persistNotified(proposal, logger); } catch (err) { diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index cc11e8e5ad..827de235a8 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -1102,7 +1102,11 @@ describe("PolymarketNotifier", function () { params.fillEventsLookbackSeconds = 7_200; const currentBlock = 2_000; - const providerStub = ({ getBlockNumber: sandbox.stub().resolves(currentBlock) } as unknown) as Provider; + const currentTimestamp = 1700000000; + const providerStub = ({ + getBlockNumber: sandbox.stub().resolves(currentBlock), + getBlock: sandbox.stub().resolves({ timestamp: currentTimestamp }), + } as unknown) as Provider; params.provider = providerStub; const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * (commonModule.POLYGON_BLOCKS_PER_HOUR / 3_600)); @@ -1178,14 +1182,13 @@ describe("PolymarketNotifier", function () { const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); const currentBlock = 10_000; - const providerStub = ({ getBlockNumber: sandbox.stub().resolves(currentBlock) } as unknown) as Provider; + const currentTimestamp = 1700000000; // seconds + const providerStub = ({ + getBlockNumber: sandbox.stub().resolves(currentBlock), + getBlock: sandbox.stub().resolves({ timestamp: currentTimestamp }), + } as unknown) as Provider; params.provider = providerStub; - // Fix Date.now() to a known timestamp for predictable calculations - const fixedNow = 1700000000 * 1000; // milliseconds - const currentTimestamp = fixedNow / 1000; // seconds - sandbox.stub(Date, "now").returns(fixedNow); - // Proposal A: created at block 5000 (older) // fromBlock for A = max(5000 + gapBlocks, 10000 - lookbackBlocks) = 5000 + 900 = 5900 // fromTimestamp for A = currentTimestamp - (currentBlock - 5900) / blocksPerSecond From dc56092f09fd461df80fb8ca83f90bfe9b0e1c87 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 29 Jan 2026 10:53:18 +0000 Subject: [PATCH 8/9] test(monitor-v2): use block timestamps for trade filter tests Update tests to fetch block timestamp after contract calls instead of using Date.now() at test start. This ensures trade timestamps are compatible with the new block-based tradeFilterFromTimestamp calculation. --- packages/monitor-v2/test/PolymarketMonitor.ts | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index 827de235a8..957d82be0a 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -426,26 +426,27 @@ describe("PolymarketNotifier", function () { }); it("It should notify if there are sell trades over the threshold", async function () { - // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter - const currentTimestamp = Math.floor(Date.now() / 1000) + 10; + mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); + mockFunctionWithReturnValue("getPolymarketMarketInformation", marketInfo); + + await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); + await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); + + // Use block timestamp after contract calls to ensure trade passes the filter + const currentBlock = await ethers.provider.getBlock("latest"); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [ { price: 0.9, type: "sell", amount: 100, - timestamp: currentTimestamp, + timestamp: currentBlock.timestamp, }, ], [], ]; - mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); - mockFunctionWithReturnValue("getPolymarketMarketInformation", marketInfo); mockSyncFunctionWithReturnValue("getOrderFilledEvents", orderFilledEvents); - await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); - await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); - const spy = sinon.spy(); const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]); await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams()); @@ -468,8 +469,14 @@ describe("PolymarketNotifier", function () { }); it("It should notify if there are buy trades over the threshold", async function () { - // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter - const currentTimestamp = Math.floor(Date.now() / 1000) + 10; + mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); + mockFunctionWithReturnValue("getPolymarketMarketInformation", marketInfo); + + await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); + await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); + + // Use block timestamp after contract calls to ensure trade passes the filter + const currentBlock = await ethers.provider.getBlock("latest"); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [], [ @@ -477,17 +484,12 @@ describe("PolymarketNotifier", function () { price: 0.1, type: "buy", amount: 100, - timestamp: currentTimestamp, + timestamp: currentBlock.timestamp, }, ], ]; - mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); - mockFunctionWithReturnValue("getPolymarketMarketInformation", marketInfo); mockSyncFunctionWithReturnValue("getOrderFilledEvents", orderFilledEvents); - await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); - await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); - const spy = sinon.spy(); const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]); await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams()); @@ -741,8 +743,14 @@ describe("PolymarketNotifier", function () { it("It should not notify if already notified", async function () { sandbox.restore(); - // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter - const currentTimestamp = Math.floor(Date.now() / 1000) + 10; + mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); + mockFunctionWithReturnValue("getPolymarketMarketInformation", marketInfo); + + await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); + const tx = await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); + + // Use block timestamp after contract calls to ensure trade passes the filter + const currentBlock = await ethers.provider.getBlock("latest"); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [], [ @@ -750,16 +758,11 @@ describe("PolymarketNotifier", function () { price: 0.1, type: "buy", amount: 100, - timestamp: currentTimestamp, + timestamp: currentBlock.timestamp, }, ], ]; - mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); - mockFunctionWithReturnValue("getPolymarketMarketInformation", marketInfo); mockSyncFunctionWithReturnValue("getOrderFilledEvents", orderFilledEvents); - - await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); - const tx = await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); const receipt = await tx.wait(); // Find the ProposePrice event to get the correct logIndex const proposePriceEvent = receipt.events?.find((e) => e.event === "ProposePrice"); @@ -814,28 +817,27 @@ describe("PolymarketNotifier", function () { }); it("It should notify two times if there are buy trades over the threshold and it's a high volume market proposal", async function () { - // Use a timestamp slightly in the future to ensure it passes the fromTimestamp filter - // (the main code computes fromTimestamp from Date.now() which runs after this) - const currentTimestamp = Math.floor(Date.now() / 1000) + 10; + mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); + mockFunctionWithReturnValue("getPolymarketMarketInformation", [{ ...marketInfo[0], volumeNum: 2_000_000 }]); + + await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); + await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); + + // Use block timestamp after contract calls to ensure trade passes the filter + const currentBlock = await ethers.provider.getBlock("latest"); const orderFilledEvents: [PolymarketTradeInformation[], PolymarketTradeInformation[]] = [ [ { price: 0.9, type: "sell", amount: 100, - timestamp: currentTimestamp, + timestamp: currentBlock.timestamp, }, ], [], ]; - - mockFunctionWithReturnValue("getPolymarketOrderBooks", asBooksRecord(emptyOrders)); - mockFunctionWithReturnValue("getPolymarketMarketInformation", [{ ...marketInfo[0], volumeNum: 2_000_000 }]); mockSyncFunctionWithReturnValue("getOrderFilledEvents", orderFilledEvents); - await oov2.requestPrice(identifier, 1, ancillaryData, votingToken.address, 0); - await oov2.proposePrice(await deployer.getAddress(), identifier, 1, ancillaryData, ONE); - const spy = sinon.spy(); const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]); await monitorTransactionsProposedOrderBook(spyLogger, await createMonitoringParams()); From 951242296faad20677b30263e00efffcf6c10635 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 29 Jan 2026 12:28:30 +0000 Subject: [PATCH 9/9] refactor(monitor-v2): use single RPC call for block number and timestamp Use getBlock("latest") instead of separate getBlockNumber() and getBlock(currentBlock) calls to reduce RPC overhead. --- .../src/monitor-polymarket/MonitorProposalsOrderBook.ts | 5 +++-- packages/monitor-v2/test/PolymarketMonitor.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts index 91190bcf58..35deca585d 100644 --- a/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts +++ b/packages/monitor-v2/src/monitor-polymarket/MonitorProposalsOrderBook.ts @@ -325,8 +325,9 @@ export async function monitorTransactionsProposedOrderBook( const lookbackBlocks = Math.round(params.fillEventsLookbackSeconds * blocksPerSecond); const gapBlocks = Math.round(params.fillEventsProposalGapSeconds * blocksPerSecond); - const currentBlock = await params.provider.getBlockNumber(); - const currentTimestamp = (await params.provider.getBlock(currentBlock)).timestamp; + const latestBlock = await params.provider.getBlock("latest"); + const currentBlock = latestBlock.number; + const currentTimestamp = latestBlock.timestamp; // Augment bundles with per-proposal context (tradeFilterFromTimestamp, aiDeeplink) const augmentedBundles = activeBundles.map(({ proposal, markets }) => { diff --git a/packages/monitor-v2/test/PolymarketMonitor.ts b/packages/monitor-v2/test/PolymarketMonitor.ts index 957d82be0a..828ad822f9 100644 --- a/packages/monitor-v2/test/PolymarketMonitor.ts +++ b/packages/monitor-v2/test/PolymarketMonitor.ts @@ -1107,7 +1107,7 @@ describe("PolymarketNotifier", function () { const currentTimestamp = 1700000000; const providerStub = ({ getBlockNumber: sandbox.stub().resolves(currentBlock), - getBlock: sandbox.stub().resolves({ timestamp: currentTimestamp }), + getBlock: sandbox.stub().resolves({ number: currentBlock, timestamp: currentTimestamp }), } as unknown) as Provider; params.provider = providerStub; @@ -1187,7 +1187,7 @@ describe("PolymarketNotifier", function () { const currentTimestamp = 1700000000; // seconds const providerStub = ({ getBlockNumber: sandbox.stub().resolves(currentBlock), - getBlock: sandbox.stub().resolves({ timestamp: currentTimestamp }), + getBlock: sandbox.stub().resolves({ number: currentBlock, timestamp: currentTimestamp }), } as unknown) as Provider; params.provider = providerStub;