diff --git a/src/contracts/providers/adaptive.types.ts b/src/contracts/providers/adaptive.types.ts index a739a4a..865c600 100644 --- a/src/contracts/providers/adaptive.types.ts +++ b/src/contracts/providers/adaptive.types.ts @@ -38,6 +38,10 @@ export type UrlHealth = { circuitOpen: boolean; /** Epoch millis until which the circuit stays open. Zero when closed. */ openUntil: number; + /** Number of timeout-specific failures recorded. Used by adaptive provider + * to weigh timeout frequency into health scoring independently from + * other transient failures. */ + timeoutCount?: number; }; /** diff --git a/src/contracts/providers/healthTracker.ts b/src/contracts/providers/healthTracker.ts index 32949e0..ec7d4eb 100644 --- a/src/contracts/providers/healthTracker.ts +++ b/src/contracts/providers/healthTracker.ts @@ -103,6 +103,25 @@ export class HealthTracker { } } + /** + * Records a timeout failure. Delegates to recordFailure for circuit-breaker + * logic and additionally increments a timeout-specific counter so the + * adaptive provider can factor timeout frequency into health scoring. + */ + public recordTimeout(url: string, now: number = Date.now()): void { + const record = this.get(url); + record.timeoutCount = (record.timeoutCount ?? 0) + 1; + this.recordFailure(url, now); + } + + /** + * Returns the number of timeout failures recorded for a URL. + */ + public timeoutCount(url: string): number { + const record = this.health.get(url); + return record?.timeoutCount ?? 0; + } + /** Current smoothed latency for a URL, or Infinity if never measured. */ public latencyOf(url: string): number { const record = this.health.get(url); diff --git a/src/contracts/providers/provider.types.ts b/src/contracts/providers/provider.types.ts index 50f7d1b..e7af048 100644 --- a/src/contracts/providers/provider.types.ts +++ b/src/contracts/providers/provider.types.ts @@ -35,3 +35,58 @@ export interface ContractProvider { */ batchEthCall(requests: EthCallRequest[], options?: RequestOptions): Promise; } + +// ─── WebSocket provider types ───────────────────────────────────────────── + +/** A decoded ERC-20 Transfer event log. */ +export type TransferEvent = { + from: string; + to: string; + value: bigint; + transactionHash: string; + blockNumber: number; +}; + +/** Callback invoked for each Transfer event received via subscription. */ +export type TransferCallback = (event: TransferEvent) => void; + +/** Configuration for the WebSocket-based contract event provider. */ +export type WebSocketProviderConfig = { + /** WebSocket endpoint URL (ws:// or wss://). */ + wssUrl: string; + + /** Maximum number of reconnection attempts before giving up. Default: 10. */ + maxReconnects?: number; + + /** Initial backoff delay in milliseconds for reconnection. Default: 1000. */ + baseDelayMs?: number; + + /** Maximum backoff delay in milliseconds. Default: 30_000. */ + maxDelayMs?: number; + + /** + * Timeout in milliseconds for individual `eth_subscribe` confirmations. + * Default: 15_000. + */ + subscribeTimeoutMs?: number; + + /** + * Per-request timeout in milliseconds for JSON-RPC calls over the + * WebSocket transport. When exceeded the request is rejected with a + * TIMEOUT error but the underlying persistent socket is NOT closed. + * Default: 10_000. + */ + requestTimeoutMs?: number; +}; + +/** + * Extension of {@link ContractProvider} that adds real-time event + * subscriptions via WebSocket. + */ +export interface SubscribableContractProvider extends ContractProvider { + /** Subscribe to Transfer events for a contract address. */ + subscribe(contractAddress: string, callback: TransferCallback): Promise<() => void>; + + /** Cleanly tear down the provider and all active subscriptions. */ + destroy(): void; +} diff --git a/src/contracts/providers/webSocketProvider.ts b/src/contracts/providers/webSocketProvider.ts index 882a785..8ee5419 100644 --- a/src/contracts/providers/webSocketProvider.ts +++ b/src/contracts/providers/webSocketProvider.ts @@ -79,6 +79,7 @@ export class WebSocketContractProvider implements SubscribableContractProvider { private readonly baseDelayMs: number; private readonly maxDelayMs: number; private readonly subscribeTimeoutMs: number; + private readonly requestTimeoutMs: number; // ----------------------------------------------------------------------- // State @@ -149,6 +150,7 @@ export class WebSocketContractProvider implements SubscribableContractProvider { this.baseDelayMs = config.baseDelayMs ?? 1000; this.maxDelayMs = config.maxDelayMs ?? 30_000; this.subscribeTimeoutMs = config.subscribeTimeoutMs ?? 15_000; + this.requestTimeoutMs = config.requestTimeoutMs ?? 10_000; this.connect(); } @@ -185,7 +187,7 @@ export class WebSocketContractProvider implements SubscribableContractProvider { GuildPassErrorCode.TIMEOUT, ), ); - }, 30_000); + }, this.requestTimeoutMs); this.pending.set(id, { resolve, reject, timer });