From 149697e2e113c8e3a9387e18e882a42635eb97b2 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 16 Dec 2025 09:37:25 -0800 Subject: [PATCH 01/16] chore(v0.9.1): release metadata for v0.9.1 contract upgrades --- contracts/deployments/7683/v0.9.1.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 contracts/deployments/7683/v0.9.1.json diff --git a/contracts/deployments/7683/v0.9.1.json b/contracts/deployments/7683/v0.9.1.json new file mode 100644 index 00000000..924c3906 --- /dev/null +++ b/contracts/deployments/7683/v0.9.1.json @@ -0,0 +1,25 @@ +{ + "metadata": { + "version": "v0.9.1", + "date": "2025-12-16", + "by": "Evan", + "from_machine": "localhost", + "from_branch": "canary", + "from_commit": "0854f70f22cebab5ce34cb3a3c5a2b9ab8dadba9" + }, + "addresses": { + "arbitrum": { + "t1ProxyAdmin": "0xb45A6e9e6920A3bfa7Ac5D6783A060bc157DEC5b", + "t1ERC7683": { + "implementation": "0x8c5CD39DbFB5f265a8370f1D5943611F427217d8" + } + }, + "base": { + "t1ProxyAdmin": "0x1a209EA791e41Bd93a8f2f772CfA04949ECD1962", + "t1ERC7683": { + "implementation": "0x8c5CD39DbFB5f265a8370f1D5943611F427217d8" + } + }, + "deployerAddress": "0xfA004a3f114641FAeE326800f0266d719e9Fc9c3" + } +} From b16761122a1bda1661d0cbb8017d1d5c850557c4 Mon Sep 17 00:00:00 2001 From: evchip Date: Tue, 16 Dec 2025 09:51:45 -0800 Subject: [PATCH 02/16] chore(v0.9.1.json): update address for second upgrade on arbitrum --- contracts/deployments/7683/v0.9.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deployments/7683/v0.9.1.json b/contracts/deployments/7683/v0.9.1.json index 924c3906..9ce05098 100644 --- a/contracts/deployments/7683/v0.9.1.json +++ b/contracts/deployments/7683/v0.9.1.json @@ -11,7 +11,7 @@ "arbitrum": { "t1ProxyAdmin": "0xb45A6e9e6920A3bfa7Ac5D6783A060bc157DEC5b", "t1ERC7683": { - "implementation": "0x8c5CD39DbFB5f265a8370f1D5943611F427217d8" + "implementation": "0x50645311C65Ee364aD37b54219d69FCBf7FF0759" } }, "base": { From c46640ee07b491f0fbd76977c0ebe142d3d5906c Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 17 Dec 2025 11:02:02 -0800 Subject: [PATCH 03/16] feat(sealed-bid-auction): add logic to capture historical event logs --- tee-apps/sealed-bid-auction/script/server.ts | 131 ++++++++++-------- .../src/blockchain/ViemIntentObserver.ts | 53 ++++++- 2 files changed, 124 insertions(+), 60 deletions(-) diff --git a/tee-apps/sealed-bid-auction/script/server.ts b/tee-apps/sealed-bid-auction/script/server.ts index 33c63d66..be51cb85 100644 --- a/tee-apps/sealed-bid-auction/script/server.ts +++ b/tee-apps/sealed-bid-auction/script/server.ts @@ -1,80 +1,97 @@ -import * as dotenv from "dotenv"; -import {AuctionApiServer} from "../src/api/AuctionApiServer.ts"; -import {ViemIntentObserver} from "../src/blockchain/ViemIntentObserver.ts"; -import {SolverPriceBook} from "../src/core/SolverPriceBook.ts"; -import {AuctionService} from "../src/core/AuctionService.ts"; -import {arbitrum, arbitrumSepolia, base, baseSepolia} from "viem/chains"; -import {BlockchainClient} from "../src/blockchain/BlockchainClient.ts"; +import * as dotenv from "dotenv" +import { AuctionApiServer } from "../src/api/AuctionApiServer.ts" +import { ViemIntentObserver } from "../src/blockchain/ViemIntentObserver.ts" +import { SolverPriceBook } from "../src/core/SolverPriceBook.ts" +import { AuctionService } from "../src/core/AuctionService.ts" +import { arbitrum, arbitrumSepolia, base, baseSepolia } from "viem/chains" +import { BlockchainClient } from "../src/blockchain/BlockchainClient.ts" -dotenv.config(); +dotenv.config() -const USE_TLS = process.env.USE_TLS as string === "true"; -const IS_MAINNET = process.env.IS_MAINNET as string === "true"; +const USE_TLS = (process.env.USE_TLS as string) === "true" +const IS_MAINNET = (process.env.IS_MAINNET as string) === "true" const SOLVER_PRICE_TTL_SECONDS = process.env.SOLVER_PRICE_TTL_SECONDS -const TEN_MINUTES_IN_SECONDS = 600; +const TEN_MINUTES_IN_SECONDS = 600 -const BASE_T1_ERC_7683_CONTRACT_ADDRESS = process.env.BASE_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}`; -const ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS = process.env.ARBITRUM_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}`; +const BASE_T1_ERC_7683_CONTRACT_ADDRESS = process.env + .BASE_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}` +const ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS = process.env + .ARBITRUM_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}` -const ARBITRUM_WS = (process.env.ARBITRUM_WS) as string; -const BASE_WS = (process.env.BASE_WS) as string; +const ARBITRUM_WS = process.env.ARBITRUM_WS as string +const BASE_WS = process.env.BASE_WS as string -const solverPriceBook = new SolverPriceBook(SOLVER_PRICE_TTL_SECONDS ? Number(SOLVER_PRICE_TTL_SECONDS as string) : TEN_MINUTES_IN_SECONDS); -const auctionService = new AuctionService(solverPriceBook); +const solverPriceBook = new SolverPriceBook( + SOLVER_PRICE_TTL_SECONDS + ? Number(SOLVER_PRICE_TTL_SECONDS as string) + : TEN_MINUTES_IN_SECONDS +) +const auctionService = new AuctionService(solverPriceBook) -const httpServer = new AuctionApiServer(solverPriceBook, auctionService); +const httpServer = new AuctionApiServer(solverPriceBook, auctionService) const arbitrumClient = new BlockchainClient( - ARBITRUM_WS, - IS_MAINNET ? arbitrum : arbitrumSepolia, - process.env.ARBITRUM_SIGNER_PRIVATE_KEY as `0x${string}` -); + ARBITRUM_WS, + IS_MAINNET ? arbitrum : arbitrumSepolia, + process.env.ARBITRUM_SIGNER_PRIVATE_KEY as `0x${string}` +) const baseClient = new BlockchainClient( - BASE_WS, - IS_MAINNET ? base : baseSepolia, - process.env.BASE_SIGNER_PRIVATE_KEY as `0x${string}` -); + BASE_WS, + IS_MAINNET ? base : baseSepolia, + process.env.BASE_SIGNER_PRIVATE_KEY as `0x${string}` +) + +const fromBlockEnv = process.env.INTENT_OBSERVER_FROM_BLOCK +const fromBlock = fromBlockEnv ? BigInt(fromBlockEnv) : null +const auctionPollingInterval = 500; + const arbitrumSepoliaIntentObserver = new ViemIntentObserver( - arbitrumClient, - ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, - auctionService, - httpServer, - baseClient.publicClient.chain.id, - BASE_T1_ERC_7683_CONTRACT_ADDRESS -); + arbitrumClient, + ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, + auctionService, + httpServer, + baseClient.publicClient.chain.id, + BASE_T1_ERC_7683_CONTRACT_ADDRESS, + auctionPollingInterval, + fromBlock +) const baseSepoliaIntentObserver = new ViemIntentObserver( - baseClient, - BASE_T1_ERC_7683_CONTRACT_ADDRESS, - auctionService, - httpServer, - arbitrumClient.publicClient.chain.id, - ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS -); + baseClient, + BASE_T1_ERC_7683_CONTRACT_ADDRESS, + auctionService, + httpServer, + arbitrumClient.publicClient.chain.id, + ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, + auctionPollingInterval, + null +) async function main() { - console.log(`Starting ${IS_MAINNET ? 'mainnet' : 'testnet'} Sealed Bid API server...`); - await httpServer.start(Number(process.env.SERVER_PORT as string), USE_TLS); - arbitrumSepoliaIntentObserver.start(); - baseSepoliaIntentObserver.start(); + console.log( + `Starting ${IS_MAINNET ? "mainnet" : "testnet"} Sealed Bid API server...` + ) + await httpServer.start(Number(process.env.SERVER_PORT as string), USE_TLS) + arbitrumSepoliaIntentObserver.start() + baseSepoliaIntentObserver.start() } async function stopAll() { - await httpServer.stop(); + await httpServer.stop() } main() - .then() - .catch(async (error) => { - await stopAll(); - console.error("", error); - process.exit(1); - }); + .then() + .catch(async (error) => { + await stopAll() + console.error("", error) + process.exit(1) + }) process.on("SIGINT", async () => { - await stopAll(); - process.exit(0); -}); + await stopAll() + process.exit(0) +}) process.on("SIGTERM", async () => { - await stopAll(); - process.exit(0); -}); + await stopAll() + process.exit(0) +}) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index 658208b2..b28ffd14 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -2,6 +2,7 @@ import { decodeAbiParameters, parseEventLogs, trim, + type Log, type WatchEventOnLogsParameter, } from "viem"; @@ -16,9 +17,11 @@ import { serialize, WinstonLogger } from "../utils/WinstonLogger.ts"; import type { BlockchainClient } from "./BlockchainClient.ts"; import type { AuctionResult } from "../api/types.ts"; +const HISTORICAL_BLOCK_CHUNK_SIZE = 100n; + export class ViemIntentObserver { private logger: WinstonLogger; - + constructor( private readonly sourceChainClient: BlockchainClient, private readonly sourceChainT1Erc7683ContractAddress: `0x${string}`, @@ -26,14 +29,23 @@ export class ViemIntentObserver { private readonly apiServer: AuctionApiServer, private readonly destinationChainId: number, private readonly destinationChainT1Erc7683ContractAddress: `0x${string}`, - private readonly auctionPollingInterval: number = 500 + private readonly auctionPollingInterval: number = 500, + private readonly fromBlock: bigint | null ) { this.logger = new WinstonLogger( `${ViemIntentObserver.name}[${this.sourceChainClient.publicClient.chain.name}]` ); } - public start() { + public async start() { + if (this.fromBlock !== null) { + await this.fetchHistoricalLogs(); + } + + this.startWatching(); + } + + private startWatching() { this.sourceChainClient.publicClient.watchEvent({ address: this.sourceChainT1Erc7683ContractAddress, event: OPEN_INTENT_ABI_EVENT, @@ -46,6 +58,41 @@ export class ViemIntentObserver { ); } + private async fetchHistoricalLogs() { + if (this.fromBlock === null) return; + + const currentBlock = await this.sourceChainClient.publicClient.getBlockNumber(); + this.logger.info( + `Fetching historical logs from block ${this.fromBlock} to ${currentBlock}` + ); + + let fromBlock = this.fromBlock; + + while (fromBlock <= currentBlock) { + const toBlock = fromBlock + HISTORICAL_BLOCK_CHUNK_SIZE - 1n > currentBlock + ? currentBlock + : fromBlock + HISTORICAL_BLOCK_CHUNK_SIZE - 1n; + + this.logger.info(`Fetching logs from block ${fromBlock} to ${toBlock}`); + + const logs = await this.sourceChainClient.publicClient.getLogs({ + address: this.sourceChainT1Erc7683ContractAddress, + event: OPEN_INTENT_ABI_EVENT, + fromBlock, + toBlock, + }); + + if (logs.length > 0) { + this.logger.info(`Found ${logs.length} historical logs in blocks ${fromBlock}-${toBlock}`); + await this.processIntentLogs(logs as unknown as WatchEventOnLogsParameter); + } + + fromBlock = toBlock + 1n; + } + + this.logger.info(`Finished fetching historical logs, caught up to block ${currentBlock}`); + } + private async processIntentLogs(logs: WatchEventOnLogsParameter) { this.logger.info("Intent was Open-ed!"); From 9c191092634fc8ee1d799f4a64bcb1ae65efd799 Mon Sep 17 00:00:00 2001 From: evchip Date: Wed, 17 Dec 2025 20:18:45 -0800 Subject: [PATCH 04/16] chore(v091): add proxy addresses of 7683 --- contracts/deployments/7683/v0.9.1.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/deployments/7683/v0.9.1.json b/contracts/deployments/7683/v0.9.1.json index 9ce05098..39341333 100644 --- a/contracts/deployments/7683/v0.9.1.json +++ b/contracts/deployments/7683/v0.9.1.json @@ -11,13 +11,15 @@ "arbitrum": { "t1ProxyAdmin": "0xb45A6e9e6920A3bfa7Ac5D6783A060bc157DEC5b", "t1ERC7683": { - "implementation": "0x50645311C65Ee364aD37b54219d69FCBf7FF0759" + "implementation": "0x50645311C65Ee364aD37b54219d69FCBf7FF0759", + "proxy": "0x996f3583BD967bbA19694733AA7A7623E6D780eb" } }, "base": { "t1ProxyAdmin": "0x1a209EA791e41Bd93a8f2f772CfA04949ECD1962", "t1ERC7683": { - "implementation": "0x8c5CD39DbFB5f265a8370f1D5943611F427217d8" + "implementation": "0x8c5CD39DbFB5f265a8370f1D5943611F427217d8", + "proxy": "0xdbA711a6c1b187479e9a5b33020E5217D0BD5A1f" } }, "deployerAddress": "0xfA004a3f114641FAeE326800f0266d719e9Fc9c3" From cfc13e0b844ca6cfa37e49161ac0268abd77db34 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 09:43:03 -0800 Subject: [PATCH 05/16] refactor(sealed-bid-auction): rewrite component to pass open event to solver --- tee-apps/sealed-bid-auction/src/api/types.ts | 13 +++++++++ .../src/blockchain/ViemIntentObserver.ts | 27 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/tee-apps/sealed-bid-auction/src/api/types.ts b/tee-apps/sealed-bid-auction/src/api/types.ts index b352444a..9b3c06d2 100644 --- a/tee-apps/sealed-bid-auction/src/api/types.ts +++ b/tee-apps/sealed-bid-auction/src/api/types.ts @@ -31,12 +31,25 @@ export interface AuctionQuote { timestamp: number; } +export interface OpenEventLog { + address: string; + topics: string[]; + data: string; + blockNumber: string; + transactionHash: string; + transactionIndex: string; + blockHash: string; + logIndex: string; + removed: boolean; +} + export interface AuctionResult { type: string; orderId: string; settlementReceiverAddress: string; amountOut: bigint; signature: string; + openEvent: OpenEventLog; } export interface AuthAttempt { diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index 658208b2..c6d055c0 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -14,7 +14,7 @@ import { import type { AuctionApiServer } from "../api/AuctionApiServer.ts"; import { serialize, WinstonLogger } from "../utils/WinstonLogger.ts"; import type { BlockchainClient } from "./BlockchainClient.ts"; -import type { AuctionResult } from "../api/types.ts"; +import type { AuctionResult, OpenEventLog } from "../api/types.ts"; export class ViemIntentObserver { private logger: WinstonLogger; @@ -54,7 +54,23 @@ export class ViemIntentObserver { logs, }); - for (const order of parsedLogs) { + for (let i = 0; i < parsedLogs.length; i++) { + const order = parsedLogs[i]; + const rawLog = logs[i]; + + // Convert raw log to eth_getLogs hex format for tokka-filler + const openEvent: OpenEventLog = { + address: rawLog.address, + topics: rawLog.topics as string[], + data: rawLog.data, + blockNumber: `0x${rawLog.blockNumber.toString(16)}`, + transactionHash: rawLog.transactionHash, + transactionIndex: `0x${rawLog.transactionIndex.toString(16)}`, + blockHash: rawLog.blockHash, + logIndex: `0x${rawLog.logIndex.toString(16)}`, + removed: rawLog.removed, + }; + for (const fillInstruction of order.args.resolvedOrder.fillInstructions) { const [decodedOrder] = decodeAbiParameters( ORDER_DATA_ABI_PARAMETERS_WRAPPED_IN_TUPLE, @@ -79,7 +95,8 @@ export class ViemIntentObserver { closedAuction: orderData.closedAuction, data: orderData.data, }, - order.args.orderId + order.args.orderId, + openEvent ); } } @@ -87,7 +104,8 @@ export class ViemIntentObserver { private async runAuctionAndNotifySolver( orderData: OrderData, - orderId: `0x${string}` + orderId: `0x${string}`, + openEvent: OpenEventLog ) { this.logger.info(`Running auction for order ${orderId}`); @@ -116,6 +134,7 @@ export class ViemIntentObserver { winningPrice.settlementReceiverAddress as `0x${string}`, winningPrice.amountOut ), + openEvent, }; await this.apiServer.notifySolvers( From ae2fa942d5235533108cbefef01cc2cb058a2762 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 11:16:49 -0800 Subject: [PATCH 06/16] refactor(sealed bid auction): add try catch block to auction call --- .../src/blockchain/ViemIntentObserver.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index 866f0a07..cd535d00 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -159,11 +159,17 @@ export class ViemIntentObserver { this.logger.debug(`Running auction for orderData ${serialize(orderData)}`); while (Date.now() / 1000 < orderData.fillDeadline) { - const winningPrice = this.auctionService.auction( - orderData.inputToken, - orderData.outputToken, - orderData.amountIn - ); + let winningPrice; + try { + winningPrice = this.auctionService.auction( + orderData.inputToken, + orderData.outputToken, + orderData.amountIn + ); + } catch (e) { + this.logger.error(`Auction error: ${e}`); + break; + } this.logger.debug(`Auction winner: ${serialize(winningPrice)}`); From 4869d69dcb84e4613b8ad6a1e083c5d73d244252 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 11:20:38 -0800 Subject: [PATCH 07/16] refactor(sealed bid auction): temp always trigger while loop --- .../sealed-bid-auction/src/blockchain/ViemIntentObserver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index cd535d00..e59b3f7d 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -158,7 +158,7 @@ export class ViemIntentObserver { this.logger.debug(`Running auction for orderData ${serialize(orderData)}`); - while (Date.now() / 1000 < orderData.fillDeadline) { + while (0 < orderData.fillDeadline) { let winningPrice; try { winningPrice = this.auctionService.auction( From 099453feed279f9b50203ab9d87e9f5662dcb410 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 11:21:04 -0800 Subject: [PATCH 08/16] feat(sealed bid auction): add log for timeout --- tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index e59b3f7d..ab53d2b1 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -205,6 +205,7 @@ export class ViemIntentObserver { setTimeout(resolve, this.auctionPollingInterval) ); } + this.logger.info(`Auction for order ${orderId} timed out`); } private async signAuctionResult( From da024410663577b69ba71d9bceb39596f69d1668 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 11:37:36 -0800 Subject: [PATCH 09/16] refactor(sealed bid app): rm temp debugging measures --- .../sealed-bid-auction/src/blockchain/ViemIntentObserver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index ab53d2b1..cd535d00 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -158,7 +158,7 @@ export class ViemIntentObserver { this.logger.debug(`Running auction for orderData ${serialize(orderData)}`); - while (0 < orderData.fillDeadline) { + while (Date.now() / 1000 < orderData.fillDeadline) { let winningPrice; try { winningPrice = this.auctionService.auction( @@ -205,7 +205,6 @@ export class ViemIntentObserver { setTimeout(resolve, this.auctionPollingInterval) ); } - this.logger.info(`Auction for order ${orderId} timed out`); } private async signAuctionResult( From 6428afb2164d4e8eac0368fa959f7cf52b026e5d Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 12:34:10 -0800 Subject: [PATCH 10/16] fix(sealed bid server.ts): unformat \& add arb / base specific from blocks --- tee-apps/sealed-bid-auction/script/server.ts | 136 +++++++++---------- 1 file changed, 65 insertions(+), 71 deletions(-) diff --git a/tee-apps/sealed-bid-auction/script/server.ts b/tee-apps/sealed-bid-auction/script/server.ts index be51cb85..d6093612 100644 --- a/tee-apps/sealed-bid-auction/script/server.ts +++ b/tee-apps/sealed-bid-auction/script/server.ts @@ -1,97 +1,91 @@ -import * as dotenv from "dotenv" -import { AuctionApiServer } from "../src/api/AuctionApiServer.ts" -import { ViemIntentObserver } from "../src/blockchain/ViemIntentObserver.ts" -import { SolverPriceBook } from "../src/core/SolverPriceBook.ts" -import { AuctionService } from "../src/core/AuctionService.ts" -import { arbitrum, arbitrumSepolia, base, baseSepolia } from "viem/chains" -import { BlockchainClient } from "../src/blockchain/BlockchainClient.ts" +import * as dotenv from "dotenv"; +import {AuctionApiServer} from "../src/api/AuctionApiServer.ts"; +import {ViemIntentObserver} from "../src/blockchain/ViemIntentObserver.ts"; +import {SolverPriceBook} from "../src/core/SolverPriceBook.ts"; +import {AuctionService} from "../src/core/AuctionService.ts"; +import {arbitrum, arbitrumSepolia, base, baseSepolia} from "viem/chains"; +import {BlockchainClient} from "../src/blockchain/BlockchainClient.ts"; -dotenv.config() +dotenv.config(); -const USE_TLS = (process.env.USE_TLS as string) === "true" -const IS_MAINNET = (process.env.IS_MAINNET as string) === "true" +const USE_TLS = process.env.USE_TLS as string === "true"; +const IS_MAINNET = process.env.IS_MAINNET as string === "true"; const SOLVER_PRICE_TTL_SECONDS = process.env.SOLVER_PRICE_TTL_SECONDS -const TEN_MINUTES_IN_SECONDS = 600 +const TEN_MINUTES_IN_SECONDS = 600; -const BASE_T1_ERC_7683_CONTRACT_ADDRESS = process.env - .BASE_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}` -const ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS = process.env - .ARBITRUM_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}` +const BASE_T1_ERC_7683_CONTRACT_ADDRESS = process.env.BASE_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}`; +const ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS = process.env.ARBITRUM_T1_ERC7683_CONTRACT_ADDRESS as `0x${string}`; -const ARBITRUM_WS = process.env.ARBITRUM_WS as string -const BASE_WS = process.env.BASE_WS as string +const ARBITRUM_WS = (process.env.ARBITRUM_WS) as string; +const BASE_WS = (process.env.BASE_WS) as string; -const solverPriceBook = new SolverPriceBook( - SOLVER_PRICE_TTL_SECONDS - ? Number(SOLVER_PRICE_TTL_SECONDS as string) - : TEN_MINUTES_IN_SECONDS -) -const auctionService = new AuctionService(solverPriceBook) +const solverPriceBook = new SolverPriceBook(SOLVER_PRICE_TTL_SECONDS ? Number(SOLVER_PRICE_TTL_SECONDS as string) : TEN_MINUTES_IN_SECONDS); +const auctionService = new AuctionService(solverPriceBook); -const httpServer = new AuctionApiServer(solverPriceBook, auctionService) +const httpServer = new AuctionApiServer(solverPriceBook, auctionService); const arbitrumClient = new BlockchainClient( - ARBITRUM_WS, - IS_MAINNET ? arbitrum : arbitrumSepolia, - process.env.ARBITRUM_SIGNER_PRIVATE_KEY as `0x${string}` -) + ARBITRUM_WS, + IS_MAINNET ? arbitrum : arbitrumSepolia, + process.env.ARBITRUM_SIGNER_PRIVATE_KEY as `0x${string}` +); const baseClient = new BlockchainClient( - BASE_WS, - IS_MAINNET ? base : baseSepolia, - process.env.BASE_SIGNER_PRIVATE_KEY as `0x${string}` -) + BASE_WS, + IS_MAINNET ? base : baseSepolia, + process.env.BASE_SIGNER_PRIVATE_KEY as `0x${string}` +); -const fromBlockEnv = process.env.INTENT_OBSERVER_FROM_BLOCK -const fromBlock = fromBlockEnv ? BigInt(fromBlockEnv) : null +const fromBlockEnvArbitrum = process.env.INTENT_OBSERVER_FROM_BLOCK_ARBITRUM; +const fromBlockArbitrum = fromBlockEnvArbitrum ? BigInt(fromBlockEnvArbitrum) : null; +const fromBlockEnvBase = process.env.INTENT_OBSERVER_FROM_BLOCK_BASE; +const fromBlockBase = fromBlockEnvBase ? BigInt(fromBlockEnvBase) : null; const auctionPollingInterval = 500; const arbitrumSepoliaIntentObserver = new ViemIntentObserver( - arbitrumClient, - ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, - auctionService, - httpServer, - baseClient.publicClient.chain.id, - BASE_T1_ERC_7683_CONTRACT_ADDRESS, - auctionPollingInterval, - fromBlock -) + arbitrumClient, + ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, + auctionService, + httpServer, + baseClient.publicClient.chain.id, + BASE_T1_ERC_7683_CONTRACT_ADDRESS, + auctionPollingInterval, + fromBlockArbitrum +); const baseSepoliaIntentObserver = new ViemIntentObserver( - baseClient, - BASE_T1_ERC_7683_CONTRACT_ADDRESS, - auctionService, - httpServer, - arbitrumClient.publicClient.chain.id, - ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, - auctionPollingInterval, - null -) + baseClient, + BASE_T1_ERC_7683_CONTRACT_ADDRESS, + auctionService, + httpServer, + arbitrumClient.publicClient.chain.id, + ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, + auctionPollingInterval, + fromBlockBase +); async function main() { - console.log( - `Starting ${IS_MAINNET ? "mainnet" : "testnet"} Sealed Bid API server...` - ) - await httpServer.start(Number(process.env.SERVER_PORT as string), USE_TLS) - arbitrumSepoliaIntentObserver.start() - baseSepoliaIntentObserver.start() + console.log(`Starting ${IS_MAINNET ? 'mainnet' : 'testnet'} Sealed Bid API server...`); + await httpServer.start(Number(process.env.SERVER_PORT as string), USE_TLS); + arbitrumSepoliaIntentObserver.start(); + baseSepoliaIntentObserver.start(); } async function stopAll() { - await httpServer.stop() + await httpServer.stop(); } main() - .then() - .catch(async (error) => { - await stopAll() - console.error("", error) - process.exit(1) - }) + .then() + .catch(async (error) => { + await stopAll(); + console.error("", error); + process.exit(1); + }); process.on("SIGINT", async () => { - await stopAll() - process.exit(0) -}) + await stopAll(); + process.exit(0); +}); process.on("SIGTERM", async () => { - await stopAll() - process.exit(0) -}) + await stopAll(); + process.exit(0); +}); \ No newline at end of file From ecfecd86aeee20240efa7940933ee19aa0606fdd Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 12:36:39 -0800 Subject: [PATCH 11/16] docs(tee-apps): update docs according to setup --- tee-apps/intent-pauser/README.md | 3 ++- tee-apps/sealed-bid-auction/.env.template | 3 +++ tee-apps/sealed-bid-auction/README.md | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tee-apps/intent-pauser/README.md b/tee-apps/intent-pauser/README.md index 631493c4..2c3c490b 100644 --- a/tee-apps/intent-pauser/README.md +++ b/tee-apps/intent-pauser/README.md @@ -11,8 +11,9 @@ bun install To configure: ```bash -cp .env.example .env +cp .env.template .env ``` + ... and fill in the values in `.env`. To run: diff --git a/tee-apps/sealed-bid-auction/.env.template b/tee-apps/sealed-bid-auction/.env.template index 959232f9..56cf86ac 100644 --- a/tee-apps/sealed-bid-auction/.env.template +++ b/tee-apps/sealed-bid-auction/.env.template @@ -7,6 +7,9 @@ SOLVER_PRICE_TTL_SECONDS=600 NEXT_NONCE_URL= IS_MAINNET=false +INTENT_OBSERVER_FROM_BLOCK_BASE= +INTENT_OBSERVER_FROM_BLOCK_ARBITRUM= + #Arbitrum ARBITRUM_WS= ARBITRUM_T1_ERC7683_CONTRACT_ADDRESS= diff --git a/tee-apps/sealed-bid-auction/README.md b/tee-apps/sealed-bid-auction/README.md index 68b89535..406d31ce 100644 --- a/tee-apps/sealed-bid-auction/README.md +++ b/tee-apps/sealed-bid-auction/README.md @@ -8,6 +8,13 @@ bun install ``` Save TLS key in `key.pem` and certificate in `cert.pem` . Consider using CloudFlare Origin Server Certificate (of course if you use CloudFlare) +To configure: + +```bash +cp .env.template .env +``` +... and fill in the values in `.env`. + ### To run: Start server: From 4870d64b29aabe0680dbbc907c30be92ecb7324c Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 12:37:39 -0800 Subject: [PATCH 12/16] refactor(tee app): add empty line at bottom of server.ts --- tee-apps/sealed-bid-auction/script/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee-apps/sealed-bid-auction/script/server.ts b/tee-apps/sealed-bid-auction/script/server.ts index d6093612..c6a45335 100644 --- a/tee-apps/sealed-bid-auction/script/server.ts +++ b/tee-apps/sealed-bid-auction/script/server.ts @@ -88,4 +88,4 @@ process.on("SIGINT", async () => { process.on("SIGTERM", async () => { await stopAll(); process.exit(0); -}); \ No newline at end of file +}); From dad7f81dd7a4b0dd3ed2ec1e451939ab3f1d13a4 Mon Sep 17 00:00:00 2001 From: evchip Date: Thu, 18 Dec 2025 12:42:17 -0800 Subject: [PATCH 13/16] docs(sealed bid app env): add description of from block env vars --- tee-apps/sealed-bid-auction/.env.template | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tee-apps/sealed-bid-auction/.env.template b/tee-apps/sealed-bid-auction/.env.template index 56cf86ac..50103748 100644 --- a/tee-apps/sealed-bid-auction/.env.template +++ b/tee-apps/sealed-bid-auction/.env.template @@ -7,6 +7,8 @@ SOLVER_PRICE_TTL_SECONDS=600 NEXT_NONCE_URL= IS_MAINNET=false +# optional from blocks for historical event logging +# these are useful if we miss events and want to rewind INTENT_OBSERVER_FROM_BLOCK_BASE= INTENT_OBSERVER_FROM_BLOCK_ARBITRUM= From f0f35c74aab6ca37f1d2414cfe9fd10d67c1b57a Mon Sep 17 00:00:00 2001 From: evchip Date: Fri, 26 Dec 2025 06:28:36 -0800 Subject: [PATCH 14/16] fix(sealed-bid-auction): add polling fallback for unreliable WS subs --- .../src/blockchain/ViemIntentObserver.ts | 249 ++++++++++++++++-- 1 file changed, 233 insertions(+), 16 deletions(-) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index cd535d00..ada50d14 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -2,6 +2,7 @@ import { decodeAbiParameters, parseEventLogs, trim, + type AbiEvent, type Log, type WatchEventOnLogsParameter, } from "viem"; @@ -18,10 +19,21 @@ import type { BlockchainClient } from "./BlockchainClient.ts"; import type { AuctionResult, OpenEventLog } from "../api/types.ts"; const HISTORICAL_BLOCK_CHUNK_SIZE = 100n; +const RECONNECT_DELAY_MS = 5000; +const HEALTHCHECK_INTERVAL_MS = 60_000; // Log health every minute +const POLLING_BACKUP_INTERVAL_MS = 30_000; // Poll every 30 seconds as backup export class ViemIntentObserver { private logger: WinstonLogger; - + private lastProcessedBlock: bigint = 0n; + private isRecovering: boolean = false; + private unwatch: (() => void) | null = null; + private startedAt: Date | null = null; + private eventsProcessed: number = 0; + private healthcheckInterval: ReturnType | null = null; + private pollingInterval: ReturnType | null = null; + private processedEventIds: Set = new Set(); + constructor( private readonly sourceChainClient: BlockchainClient, private readonly sourceChainT1Erc7683ContractAddress: `0x${string}`, @@ -38,72 +50,277 @@ export class ViemIntentObserver { } public async start() { - if (this.fromBlock !== null) { + const chainName = this.sourceChainClient.publicClient.chain.name; + this.startedAt = new Date(); + + this.logger.info(`🚀 Starting ViemIntentObserver on ${chainName}`); + + // Initialize lastProcessedBlock to current block if not doing historical fetch + if (this.fromBlock === null) { + this.lastProcessedBlock = await this.sourceChainClient.publicClient.getBlockNumber(); + this.logger.info(`Initialized lastProcessedBlock to ${this.lastProcessedBlock}`); + } else { await this.fetchHistoricalLogs(); } this.startWatching(); + this.startHealthcheck(); + this.startPollingBackup(); + } + + private startHealthcheck() { + const chainName = this.sourceChainClient.publicClient.chain.name; + + this.healthcheckInterval = setInterval(async () => { + const uptime = this.startedAt + ? Math.floor((Date.now() - this.startedAt.getTime()) / 1000) + : 0; + const uptimeStr = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`; + + try { + const currentBlock = await this.sourceChainClient.publicClient.getBlockNumber(); + const blocksBehind = currentBlock - this.lastProcessedBlock; + + this.logger.info( + `💚 HEALTHCHECK [${chainName}] | uptime=${uptimeStr} | lastBlock=${this.lastProcessedBlock} | currentBlock=${currentBlock} | behind=${blocksBehind} | eventsProcessed=${this.eventsProcessed} | recovering=${this.isRecovering}` + ); + + // Warn if too far behind + if (blocksBehind > 1000n) { + this.logger.warn( + `⚠️ ${chainName} is ${blocksBehind} blocks behind! May have missed events.` + ); + } + } catch (e) { + this.logger.error(`Healthcheck failed on ${chainName}: ${e}`); + } + }, HEALTHCHECK_INTERVAL_MS); + } + + public stop() { + if (this.unwatch) { + this.unwatch(); + this.unwatch = null; + } + if (this.healthcheckInterval) { + clearInterval(this.healthcheckInterval); + this.healthcheckInterval = null; + } + if (this.pollingInterval) { + clearInterval(this.pollingInterval); + this.pollingInterval = null; + } + this.logger.info(`Stopped ViemIntentObserver`); + } + + private startPollingBackup() { + const chainName = this.sourceChainClient.publicClient.chain.name; + + this.logger.info(`Starting polling backup on ${chainName} (every ${POLLING_BACKUP_INTERVAL_MS}ms)`); + + this.pollingInterval = setInterval(async () => { + try { + const currentBlock = await this.sourceChainClient.publicClient.getBlockNumber(); + + // Only poll if we have a valid lastProcessedBlock and are potentially behind + if (this.lastProcessedBlock > 0n && currentBlock > this.lastProcessedBlock) { + const fromBlock = this.lastProcessedBlock + 1n; + + this.logger.debug(`Polling backup: checking blocks ${fromBlock} to ${currentBlock}`); + + const logs = await this.sourceChainClient.publicClient.getLogs({ + address: this.sourceChainT1Erc7683ContractAddress, + event: OPEN_INTENT_ABI_EVENT as AbiEvent, + fromBlock, + toBlock: currentBlock, + }); + + if (logs.length > 0) { + this.logger.info(`🔍 Polling backup found ${logs.length} events in blocks ${fromBlock}-${currentBlock}`); + + // Update lastProcessedBlock + const maxBlock = logs.reduce( + (max, log) => (log.blockNumber > max ? log.blockNumber : max), + 0n + ); + this.lastProcessedBlock = maxBlock; + + await this.processIntentLogs(logs as unknown as WatchEventOnLogsParameter); + } else { + // Even if no events, update lastProcessedBlock so we don't re-scan + this.lastProcessedBlock = currentBlock; + } + } + } catch (e) { + this.logger.error(`Polling backup failed on ${chainName}: ${e}`); + } + }, POLLING_BACKUP_INTERVAL_MS); } private startWatching() { - this.sourceChainClient.publicClient.watchEvent({ + const chainName = this.sourceChainClient.publicClient.chain.name; + + this.unwatch = this.sourceChainClient.publicClient.watchEvent({ address: this.sourceChainT1Erc7683ContractAddress, event: OPEN_INTENT_ABI_EVENT, - onLogs: (logs) => this.processIntentLogs(logs), - onError: (error) => this.logger.error(`Error from publicClient.watchEvent: ${error}`), + onLogs: (logs) => { + // Track the last processed block for recovery + if (logs.length > 0) { + const maxBlock = logs.reduce( + (max, log) => (log.blockNumber > max ? log.blockNumber : max), + 0n + ); + this.lastProcessedBlock = maxBlock; + } + this.processIntentLogs(logs); + }, + onError: (error: Error) => this.handleWatchError(error), }); this.logger.info( - `Watching for Open Intent events on chain [${this.sourceChainClient.publicClient.chain.name}] and contract [${this.sourceChainT1Erc7683ContractAddress}]` + `Watching for Open Intent events on chain [${chainName}] and contract [${this.sourceChainT1Erc7683ContractAddress}]` + ); + } + + private async handleWatchError(error: Error) { + const chainName = this.sourceChainClient.publicClient.chain.name; + + this.logger.error( + `🚨 WEBSOCKET DROPPED on ${chainName}! Error: ${error.message}. Last processed block: ${this.lastProcessedBlock}` ); + + if (this.isRecovering) { + this.logger.warn(`Already recovering, skipping duplicate recovery attempt`); + return; + } + + this.isRecovering = true; + + try { + if (this.unwatch) { + this.unwatch(); + this.unwatch = null; + } + + this.logger.info(`Waiting ${RECONNECT_DELAY_MS}ms before recovery...`); + await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY_MS)); + + if (this.lastProcessedBlock > 0n) { + this.logger.info( + `🔄 Recovering missed events from block ${this.lastProcessedBlock + 1n} on ${chainName}` + ); + await this.fetchHistoricalLogsFromBlock(this.lastProcessedBlock + 1n); + } + + this.logger.info(`✅ Restarting event watcher on ${chainName}`); + this.startWatching(); + + this.logger.info( + `✅ Recovery complete on ${chainName}. Resumed watching from block ${this.lastProcessedBlock}` + ); + } catch (recoveryError) { + this.logger.error( + `🚨 RECOVERY FAILED on ${chainName}: ${recoveryError}. Will retry in ${RECONNECT_DELAY_MS}ms` + ); + // Schedule another recovery attempt + setTimeout(() => { + this.isRecovering = false; + this.handleWatchError(error); + }, RECONNECT_DELAY_MS); + return; + } + + this.isRecovering = false; } private async fetchHistoricalLogs() { if (this.fromBlock === null) return; + await this.fetchHistoricalLogsFromBlock(this.fromBlock); + } + private async fetchHistoricalLogsFromBlock(startBlock: bigint) { + const chainName = this.sourceChainClient.publicClient.chain.name; const currentBlock = await this.sourceChainClient.publicClient.getBlockNumber(); + this.logger.info( - `Fetching historical logs from block ${this.fromBlock} to ${currentBlock}` + `Fetching historical logs on ${chainName} from block ${startBlock} to ${currentBlock}` ); - let fromBlock = this.fromBlock; + let fromBlock = startBlock; + let totalLogsFound = 0; while (fromBlock <= currentBlock) { const toBlock = fromBlock + HISTORICAL_BLOCK_CHUNK_SIZE - 1n > currentBlock ? currentBlock : fromBlock + HISTORICAL_BLOCK_CHUNK_SIZE - 1n; - this.logger.info(`Fetching logs from block ${fromBlock} to ${toBlock}`); + this.logger.debug(`Fetching logs from block ${fromBlock} to ${toBlock}`); const logs = await this.sourceChainClient.publicClient.getLogs({ address: this.sourceChainT1Erc7683ContractAddress, - event: OPEN_INTENT_ABI_EVENT, + event: OPEN_INTENT_ABI_EVENT as AbiEvent, fromBlock, toBlock, }); if (logs.length > 0) { + totalLogsFound += logs.length; this.logger.info(`Found ${logs.length} historical logs in blocks ${fromBlock}-${toBlock}`); + + const maxBlock = logs.reduce( + (max, log) => (log.blockNumber > max ? log.blockNumber : max), + 0n + ); + this.lastProcessedBlock = maxBlock; + await this.processIntentLogs(logs as unknown as WatchEventOnLogsParameter); } fromBlock = toBlock + 1n; } - this.logger.info(`Finished fetching historical logs, caught up to block ${currentBlock}`); + this.lastProcessedBlock = currentBlock; + + this.logger.info( + `Finished fetching historical logs on ${chainName}. Found ${totalLogsFound} events, caught up to block ${currentBlock}` + ); } private async processIntentLogs(logs: WatchEventOnLogsParameter) { - this.logger.info("Intent was Open-ed!"); - const parsedLogs = parseEventLogs({ abi: [OPEN_INTENT_ABI_EVENT], logs, }); + // Filter out already-processed events (dedup between WebSocket and polling) + const newLogs: { order: typeof parsedLogs[number]; rawLog: Log }[] = []; for (let i = 0; i < parsedLogs.length; i++) { - const order = parsedLogs[i]; - const rawLog = logs[i]; + const rawLog = logs[i] as Log; + const eventId = `${rawLog.transactionHash}-${rawLog.logIndex}`; + + if (this.processedEventIds.has(eventId)) { + this.logger.debug(`Skipping already-processed event: ${eventId}`); + continue; + } + + this.processedEventIds.add(eventId); + newLogs.push({ order: parsedLogs[i], rawLog }); + + // Limit memory usage: keep only last 10k event IDs + if (this.processedEventIds.size > 10000) { + const firstId = this.processedEventIds.values().next().value; + if (firstId) this.processedEventIds.delete(firstId); + } + } + + if (newLogs.length === 0) { + return; + } + + this.eventsProcessed += newLogs.length; + this.logger.info(`Intent was Open-ed! (${newLogs.length} new event(s), total processed: ${this.eventsProcessed})`); + + for (const { order, rawLog } of newLogs) { // Convert raw log to eth_getLogs hex format for tokka-filler const openEvent: OpenEventLog = { @@ -142,7 +359,7 @@ export class ViemIntentObserver { closedAuction: orderData.closedAuction, data: orderData.data, }, - order.args.orderId, + order.args?.orderId as `0x${string}`, openEvent ); } From 012231bfa6014b259aa8fe66ed9218669f9d7221 Mon Sep 17 00:00:00 2001 From: evchip Date: Fri, 26 Dec 2025 06:32:16 -0800 Subject: [PATCH 15/16] fix(sealed bid auction): fix potential undefined issue --- .../src/blockchain/ViemIntentObserver.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index ada50d14..cede1767 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -295,7 +295,10 @@ export class ViemIntentObserver { // Filter out already-processed events (dedup between WebSocket and polling) const newLogs: { order: typeof parsedLogs[number]; rawLog: Log }[] = []; for (let i = 0; i < parsedLogs.length; i++) { - const rawLog = logs[i] as Log; + const rawLog = logs[i]; + const order = parsedLogs[i]; + if (!rawLog || !order) continue; + const eventId = `${rawLog.transactionHash}-${rawLog.logIndex}`; if (this.processedEventIds.has(eventId)) { @@ -304,7 +307,7 @@ export class ViemIntentObserver { } this.processedEventIds.add(eventId); - newLogs.push({ order: parsedLogs[i], rawLog }); + newLogs.push({ order, rawLog: rawLog as Log }); // Limit memory usage: keep only last 10k event IDs if (this.processedEventIds.size > 10000) { From 610ef0f3540f67c94447628dc16230ab57e6d09e Mon Sep 17 00:00:00 2001 From: evchip Date: Mon, 29 Dec 2025 07:48:43 -0800 Subject: [PATCH 16/16] feat(sealed bid app): add pagerduty notifier for failed websocket --- tee-apps/sealed-bid-auction/.env.template | 3 + tee-apps/sealed-bid-auction/script/server.ts | 7 ++- .../src/blockchain/ViemIntentObserver.ts | 56 ++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/tee-apps/sealed-bid-auction/.env.template b/tee-apps/sealed-bid-auction/.env.template index 50103748..e869caa6 100644 --- a/tee-apps/sealed-bid-auction/.env.template +++ b/tee-apps/sealed-bid-auction/.env.template @@ -7,6 +7,9 @@ SOLVER_PRICE_TTL_SECONDS=600 NEXT_NONCE_URL= IS_MAINNET=false +#PagerDuty (optional - for alerting on WS failures) +PD_INTEGRATION_KEY= + # optional from blocks for historical event logging # these are useful if we miss events and want to rewind INTENT_OBSERVER_FROM_BLOCK_BASE= diff --git a/tee-apps/sealed-bid-auction/script/server.ts b/tee-apps/sealed-bid-auction/script/server.ts index c6a45335..e65dfa69 100644 --- a/tee-apps/sealed-bid-auction/script/server.ts +++ b/tee-apps/sealed-bid-auction/script/server.ts @@ -39,6 +39,7 @@ const fromBlockArbitrum = fromBlockEnvArbitrum ? BigInt(fromBlockEnvArbitrum) : const fromBlockEnvBase = process.env.INTENT_OBSERVER_FROM_BLOCK_BASE; const fromBlockBase = fromBlockEnvBase ? BigInt(fromBlockEnvBase) : null; const auctionPollingInterval = 500; +const pagerDutyIntegrationKey = process.env.PD_INTEGRATION_KEY || null; const arbitrumSepoliaIntentObserver = new ViemIntentObserver( arbitrumClient, @@ -48,7 +49,8 @@ const arbitrumSepoliaIntentObserver = new ViemIntentObserver( baseClient.publicClient.chain.id, BASE_T1_ERC_7683_CONTRACT_ADDRESS, auctionPollingInterval, - fromBlockArbitrum + fromBlockArbitrum, + pagerDutyIntegrationKey ); const baseSepoliaIntentObserver = new ViemIntentObserver( baseClient, @@ -58,7 +60,8 @@ const baseSepoliaIntentObserver = new ViemIntentObserver( arbitrumClient.publicClient.chain.id, ARBITRUM_T1_ERC_7683_CONTRACT_ADDRESS, auctionPollingInterval, - fromBlockBase + fromBlockBase, + pagerDutyIntegrationKey ); async function main() { diff --git a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts index bca395b6..58aa0d72 100644 --- a/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts +++ b/tee-apps/sealed-bid-auction/src/blockchain/ViemIntentObserver.ts @@ -22,6 +22,7 @@ const HISTORICAL_BLOCK_CHUNK_SIZE = 100n; const RECONNECT_DELAY_MS = 5000; const HEALTHCHECK_INTERVAL_MS = 60_000; // Log health every minute const POLLING_BACKUP_INTERVAL_MS = 30_000; // Poll every 30 seconds as backup +const PAGERDUTY_EVENTS_URL = "https://events.pagerduty.com/v2/enqueue"; export class ViemIntentObserver { private logger: WinstonLogger; @@ -42,7 +43,8 @@ export class ViemIntentObserver { private readonly destinationChainId: number, private readonly destinationChainT1Erc7683ContractAddress: `0x${string}`, private readonly auctionPollingInterval: number = 500, - private readonly fromBlock: bigint | null + private readonly fromBlock: bigint | null, + private readonly pagerDutyIntegrationKey: string | null = null ) { this.logger = new WinstonLogger( `${ViemIntentObserver.name}[${this.sourceChainClient.publicClient.chain.name}]` @@ -189,6 +191,13 @@ export class ViemIntentObserver { `🚨 WEBSOCKET DROPPED on ${chainName}! Error: ${error.message}. Last processed block: ${this.lastProcessedBlock}` ); + // Fire PagerDuty alert + await this.sendPagerDutyAlert( + `WebSocket dropped on ${chainName}`, + `Error: ${error.message}. Last processed block: ${this.lastProcessedBlock}`, + `ws-drop-${chainName}` + ); + if (this.isRecovering) { this.logger.warn(`Already recovering, skipping duplicate recovery attempt`); return; @@ -222,6 +231,14 @@ export class ViemIntentObserver { this.logger.error( `🚨 RECOVERY FAILED on ${chainName}: ${recoveryError}. Will retry in ${RECONNECT_DELAY_MS}ms` ); + + // Fire PagerDuty alert for recovery failure + await this.sendPagerDutyAlert( + `WebSocket recovery FAILED on ${chainName}`, + `Recovery error: ${recoveryError}. Will retry in ${RECONNECT_DELAY_MS}ms`, + `ws-recovery-fail-${chainName}` + ); + // Schedule another recovery attempt setTimeout(() => { this.isRecovering = false; @@ -456,4 +473,41 @@ export class ViemIntentObserver { return await this.sourceChainClient.signTypedData(domain, types, message); } + + private async sendPagerDutyAlert( + summary: string, + details: string, + dedupKey: string + ): Promise { + if (!this.pagerDutyIntegrationKey) { + this.logger.debug("PagerDuty integration key not configured, skipping alert"); + return; + } + + try { + const response = await fetch(PAGERDUTY_EVENTS_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + routing_key: this.pagerDutyIntegrationKey, + event_action: "trigger", + dedup_key: `sealed-bid-auction-${dedupKey}`, + payload: { + summary, + source: "sealed-bid-auction", + severity: "critical", + custom_details: { details }, + }, + }), + }); + + if (!response.ok) { + this.logger.error(`PagerDuty alert failed: ${response.status} ${response.statusText}`); + } else { + this.logger.info(`PagerDuty alert sent: ${summary}`); + } + } catch (e) { + this.logger.error(`Failed to send PagerDuty alert: ${e}`); + } + } }