diff --git a/.github/workflows/deploy-faucet.yml b/.github/workflows/deploy-faucet.yml index 624302c778..93a9ea6b43 100644 --- a/.github/workflows/deploy-faucet.yml +++ b/.github/workflows/deploy-faucet.yml @@ -81,6 +81,7 @@ jobs: export FAUCET_RATE_LIMIT_WINDOW_SECONDS=86400 export TXM_DB_PATH=/home/faucet/txm.sqlite export FAUCET_DB_PATH=/home/faucet/faucet.sqlite + export OTEL_EXPORTER_OTLP_ENDPOINT=http://148.113.212.211:4318/v1/traces EOF # cf. https://github.com/appleboy/drone-ssh/issues/175 sed -i '/DRONE_SSH_PREV_COMMAND_EXIT_CODE/d' .env diff --git a/.github/workflows/deploy-monitor-service.yml b/.github/workflows/deploy-monitor-service.yml index 53cb16f934..178e12df33 100644 --- a/.github/workflows/deploy-monitor-service.yml +++ b/.github/workflows/deploy-monitor-service.yml @@ -66,8 +66,8 @@ jobs: export BLOCK_TIME=2 export CHAIN_ID=216 export RPC_URL=https://rpc.testnet.happy.tech/http - export MONITOR_ADDRESSES=0x10EBe5E4E8b4B5413D8e1f91A21cE4143B6bd8F5,0x3cBD2130C2D4D6aDAA9c9054360C29e00d99f0BA,0xBAc858b1AD51527F3c4A22f146246c9913e97cFd,0x84dcb507875af1786bb6623a625d3f9aae9fda4f,0xAE45fD410bf09f27DA574D3EF547567A479F4594,0x71E30C67d58015293f452468E4754b18bAFFd807,0xE55b09F1b78B72515ff1d1a0E3C14AD5D707fdE8,0x634de6fbFfE60EE6D1257f6be3E8AF4CfefEf697 - export FUND_THRESHOLD=10000000000000000 + export MONITOR_ADDRESSES=0x10EBe5E4E8b4B5413D8e1f91A21cE4143B6bd8F5,0x3cBD2130C2D4D6aDAA9c9054360C29e00d99f0BA,0xBAc858b1AD51527F3c4A22f146246c9913e97cFd,0x84dcb507875af1786bb6623a625d3f9aae9fda4f,0xAE45fD410bf09f27DA574D3EF547567A479F4594,0x71E30C67d58015293f452468E4754b18bAFFd807,0xE55b09F1b78B72515ff1d1a0E3C14AD5D707fdE8,0x634de6fbFfE60EE6D1257f6be3E8AF4CfefEf697,0xe8bD127b013600E5c6f864e5C07E918fa80BFF89 + export FUND_THRESHOLD=100000000000000000 export FUNDS_TO_SEND=10000000000000000 export TXM_DB_PATH=/home/monitor_service/txm.sqlite export RPCS_TO_MONITOR=https://rpc.testnet.happy.tech/http,http://148.113.212.211:8545 diff --git a/Makefile b/Makefile index 2440e926bf..67085a799d 100644 --- a/Makefile +++ b/Makefile @@ -136,7 +136,7 @@ nuke: clean ## Removes build artifacts and dependencies $(MAKE) remove-modules .PHONY: nuke -test: sdk.test iframe.test ## Run tests +test: sdk.test iframe.test txm.test ## Run tests .PHONY: test test.all: test contracts.test @@ -266,17 +266,21 @@ sdk.test: $(call forall_make , $(SDK_PKGS) , test) .PHONY: sdk.test -sdk.check: - $(call forall_make , $(SDK_PKGS) , check) -.PHONY: sdk.check - iframe.test: $(call forall_make , $(IFRAME_PKGS) , test) .PHONY: iframe.test +txm.test: + cd packages/txm && make test +.PHONY: iframe.test + # ================================================================================================== # FORMATTING +sdk.check: + $(call forall_make , $(SDK_PKGS) , check) +.PHONY: sdk.check + iframe.check: $(call forall_make , $(IFRAME_PKGS) , check) .PHONY: iframe.check diff --git a/apps/faucet/src/env.ts b/apps/faucet/src/env.ts index 1971bfeb63..fd1d81f466 100644 --- a/apps/faucet/src/env.ts +++ b/apps/faucet/src/env.ts @@ -24,6 +24,7 @@ const envSchema = z.object({ TOKEN_AMOUNT: z.string().transform((s) => BigInt(s)), FAUCET_DB_PATH: z.string().trim(), FAUCET_RATE_LIMIT_WINDOW_SECONDS: z.string().transform((s) => Number(s)), + OTEL_EXPORTER_OTLP_ENDPOINT: z.string().trim().optional(), }) const parsedEnv = envSchema.safeParse(process.env) diff --git a/apps/faucet/src/services/faucet.ts b/apps/faucet/src/services/faucet.ts index fd9f838f90..09fb27e8dd 100644 --- a/apps/faucet/src/services/faucet.ts +++ b/apps/faucet/src/services/faucet.ts @@ -1,5 +1,6 @@ import type { Address } from "@happy.tech/common" import { TransactionManager, TransactionStatus } from "@happy.tech/txm" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" import { type Result, err, ok } from "neverthrow" import { env } from "../env" import { FaucetRateLimitError } from "../errors" @@ -16,6 +17,15 @@ export class FaucetService { chainId: env.CHAIN_ID, blockTime: env.BLOCK_TIME, privateKey: env.PRIVATE_KEY, + traces: { + active: true, + spanExporter: env.OTEL_EXPORTER_OTLP_ENDPOINT + ? new OTLPTraceExporter({ + url: env.OTEL_EXPORTER_OTLP_ENDPOINT, + }) + : undefined, + serviceName: "faucet", + }, }) this.faucetUsageRepository = new FaucetUsageRepository() } @@ -39,9 +49,6 @@ export class FaucetService { } } - const faucetUsage = FaucetUsage.create(address) - await this.faucetUsageRepository.save(faucetUsage) - const tx = await this.txm.createTransaction({ address, value: env.TOKEN_AMOUNT, @@ -60,6 +67,9 @@ export class FaucetService { return err(new Error("Transaction failed")) } + const faucetUsage = FaucetUsage.create(address) + await this.faucetUsageRepository.save(faucetUsage) + return ok(undefined) } } diff --git a/packages/txm/lib/HookManager.ts b/packages/txm/lib/HookManager.ts index 2f31a21d1d..6abb3e25ba 100644 --- a/packages/txm/lib/HookManager.ts +++ b/packages/txm/lib/HookManager.ts @@ -4,6 +4,15 @@ import type { Transaction } from "./Transaction.js" import type { AttemptSubmissionErrorCause } from "./TransactionSubmitter" import { TraceMethod } from "./telemetry/traces" +// TODO +// +// TransactionSubmissionFailed is weird: it only triggers when the first attempt fails to submit in the Tx, but the +// TxMonitor will still make further attempts (whose failure are not reported via hooks). +// +// TransactionSaveFailed has the same quirk, but there it makes sense as failing to save the transaction at the +// collection stage means there won't be any further attempts, whereas further failures are not really user-relevant. +// This is only used in tests, and probably this shouldn't be a user-exposed hook. + export enum TxmHookType { All = "All", TransactionStatusChanged = "TransactionStatusChanged", diff --git a/packages/txm/lib/Transaction.ts b/packages/txm/lib/Transaction.ts index 7510e16316..04eac9d018 100644 --- a/packages/txm/lib/Transaction.ts +++ b/packages/txm/lib/Transaction.ts @@ -13,7 +13,12 @@ import { TraceMethod } from "./telemetry/traces" export enum TransactionStatus { /** - * Default state for new transaction: the transaction is awaiting processing by TXM or has been submitted in the mempool and is waiting to be included in a block. + * Default state for new transaction: we're awaiting submission of the first attempt for the transaction. + */ + NotAttempted = "NotAttempted", + /** + * At least one attempt to submit the transaction has been made — it might have hit the mempool and waiting for + * inclusion in a block, or it might have failed before that. */ Pending = "Pending", /** @@ -62,7 +67,11 @@ export enum TransactionCallDataFormat { Function = "Function", } -export const NotFinalizedStatuses = [TransactionStatus.Pending, TransactionStatus.Cancelling] +export const NotFinalizedStatuses = [ + TransactionStatus.NotAttempted, + TransactionStatus.Pending, + TransactionStatus.Cancelling, +] interface TransactionConstructorBaseConfig { /** @@ -181,7 +190,7 @@ export class Transaction { this.address = config.address this.value = config.value ?? 0n this.deadline = config.deadline - this.status = config.status ?? TransactionStatus.Pending + this.status = config.status ?? TransactionStatus.NotAttempted this.attempts = config.attempts ?? [] this.collectionBlock = config.collectionBlock this.createdAt = config.createdAt ?? new Date() diff --git a/packages/txm/lib/TransactionCollector.ts b/packages/txm/lib/TransactionCollector.ts index 1c75c6987f..0ea7db6123 100644 --- a/packages/txm/lib/TransactionCollector.ts +++ b/packages/txm/lib/TransactionCollector.ts @@ -95,7 +95,7 @@ export class TransactionCollector { }) if (transaction.status === TransactionStatus.Interrupted) { - transaction.changeStatus(TransactionStatus.Pending) + transaction.changeStatus(TransactionStatus.NotAttempted) } const submissionResult = await this.txmgr.transactionSubmitter.submitNewAttempt(transaction, { @@ -105,6 +105,9 @@ export class TransactionCollector { maxPriorityFeePerGas, }) + // Only after submitting the initial attempt to avoid concurrent attempts here & in the TxMonitor. + transaction.changeStatus(TransactionStatus.Pending) + if (submissionResult.isErr()) { eventBus.emit(Topics.TransactionSubmissionFailed, { transaction, diff --git a/packages/txm/lib/TransactionManager.ts b/packages/txm/lib/TransactionManager.ts index cfc4e62284..8d909b6cce 100644 --- a/packages/txm/lib/TransactionManager.ts +++ b/packages/txm/lib/TransactionManager.ts @@ -252,6 +252,12 @@ export type TransactionManagerConfig = { * Defaults to a console span exporter. */ spanExporter?: SpanExporter + + /** + * The service name to use for the traces. + * Defaults to "txm". + */ + serviceName?: string } } @@ -307,6 +313,7 @@ export class TransactionManager { userMetricReader: _config.metrics?.metricReader, tracesActive: _config.traces?.active ?? false, userTraceExporter: _config.traces?.spanExporter, + serviceName: _config.traces?.serviceName, }) this.collectors = [] diff --git a/packages/txm/lib/TransactionRepository.ts b/packages/txm/lib/TransactionRepository.ts index 9add5d1059..a1c91e83b9 100644 --- a/packages/txm/lib/TransactionRepository.ts +++ b/packages/txm/lib/TransactionRepository.ts @@ -3,7 +3,7 @@ import type { UUID } from "@happy.tech/common" import { SpanStatusCode, context, trace } from "@opentelemetry/api" import { type Result, ResultAsync, err, ok } from "neverthrow" import { Topics, eventBus } from "./EventBus.js" -import { NotFinalizedStatuses, Transaction } from "./Transaction.js" +import { NotFinalizedStatuses, Transaction, TransactionStatus } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" import { db } from "./db/driver.js" import { TxmMetrics } from "./telemetry/metrics" @@ -42,9 +42,11 @@ export class TransactionRepository { } } - @TraceMethod("txm.transaction-repository.get-not-finalized-transactions-older-than") - getNotFinalizedTransactionsOlderThan(blockNumber: bigint): Transaction[] { - return this.notFinalizedTransactions.filter((t) => t.collectionBlock && t.collectionBlock < blockNumber) + @TraceMethod("txm.transaction-repository.get-in-flight-transactions-older-than") + getInFlightTransactionsOlderThan(blockNumber: bigint): Transaction[] { + return this.notFinalizedTransactions.filter( + (t) => t.status !== TransactionStatus.NotAttempted && t.collectionBlock! < blockNumber, + ) } @TraceMethod("txm.transaction-repository.get-transaction") diff --git a/packages/txm/lib/TxMonitor.ts b/packages/txm/lib/TxMonitor.ts index 1e30e9cd97..120dd3b3d0 100644 --- a/packages/txm/lib/TxMonitor.ts +++ b/packages/txm/lib/TxMonitor.ts @@ -81,6 +81,7 @@ export class TxMonitor { @TraceMethod("txm.tx-monitor.handle-new-block") private async handleNewBlock(block: LatestBlock) { const span = trace.getSpan(context.active())! + const txRepository = this.transactionManager.transactionRepository span.addEvent("txm.tx-monitor.handle-new-block.started", { blockNumber: Number(block.number), @@ -93,9 +94,7 @@ export class TxMonitor { return } - const transactions = this.transactionManager.transactionRepository.getNotFinalizedTransactionsOlderThan( - block.number, - ) + const transactions = txRepository.getInFlightTransactionsOlderThan(block.number) for (const transaction of transactions) { span.addEvent("txm.tx-monitor.handle-new-block.monitoring-transaction", { @@ -228,10 +227,7 @@ export class TxMonitor { await Promise.all(promises) - const result = await ResultAsync.fromPromise( - this.transactionManager.transactionRepository.saveTransactions(transactions), - unknownToError, - ) + const result = await ResultAsync.fromPromise(txRepository.saveTransactions(transactions), unknownToError) if (result.isErr()) { logger.error("Error flushing transactions in onNewBlock") diff --git a/packages/txm/lib/telemetry/instrumentation.ts b/packages/txm/lib/telemetry/instrumentation.ts index 7c107ce126..d86b5e9b73 100644 --- a/packages/txm/lib/telemetry/instrumentation.ts +++ b/packages/txm/lib/telemetry/instrumentation.ts @@ -4,13 +4,7 @@ import type { MetricReader } from "@opentelemetry/sdk-metrics" import { NodeSDK } from "@opentelemetry/sdk-node" import { ConsoleSpanExporter, type SpanExporter } from "@opentelemetry/sdk-trace-node" import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions" - -const resource = Resource.default().merge( - new Resource({ - [ATTR_SERVICE_NAME]: "txm", - [ATTR_SERVICE_VERSION]: "0.1.0", - }), -) +import { version } from "../../package.json" export function initializeTelemetry({ metricsActive, @@ -18,13 +12,22 @@ export function initializeTelemetry({ userMetricReader, tracesActive, userTraceExporter, + serviceName, }: { metricsActive: boolean prometheusPort: number userMetricReader?: MetricReader userTraceExporter?: SpanExporter tracesActive?: boolean + serviceName?: string }): void { + const resource = Resource.default().merge( + new Resource({ + [ATTR_SERVICE_NAME]: serviceName ?? "txm", + [ATTR_SERVICE_VERSION]: version, + }), + ) + let metricReader: MetricReader | undefined if (metricsActive) { metricReader = userMetricReader || new PrometheusExporter({ port: prometheusPort }) diff --git a/packages/txm/lib/utils/safeViemClients.ts b/packages/txm/lib/utils/safeViemClients.ts index 6bc57ea723..d1b4fe1a5e 100644 --- a/packages/txm/lib/utils/safeViemClients.ts +++ b/packages/txm/lib/utils/safeViemClients.ts @@ -1,6 +1,7 @@ import { bigIntReplacer, unknownToError } from "@happy.tech/common" +import { isNullish } from "@happy.tech/common" import type { Counter, Histogram, Tracer } from "@opentelemetry/api" -import { ResultAsync } from "neverthrow" +import { ResultAsync, errAsync, okAsync } from "neverthrow" import type { Account, Chain, @@ -133,7 +134,8 @@ export function convertToSafeViemPublicClient( const startTime = Date.now() return ResultAsync.fromPromise(client.estimateGas(...args), unknownToError) - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "estimateGas" }) @@ -141,7 +143,6 @@ export function convertToSafeViemPublicClient( result: JSON.stringify(result, bigIntReplacer), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -159,7 +160,8 @@ export function convertToSafeViemPublicClient( const startTime = Date.now() return ResultAsync.fromPromise(client.getTransactionReceipt(...args), unknownToError) - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "getTransactionReceipt" }) @@ -167,7 +169,6 @@ export function convertToSafeViemPublicClient( result: JSON.stringify(result, bigIntReplacer), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -191,6 +192,7 @@ export function convertToSafeViemPublicClient( }), unknownToError, ) + .andThen(errorOnNullish) .map((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) @@ -215,7 +217,8 @@ export function convertToSafeViemPublicClient( const startTime = Date.now() return ResultAsync.fromPromise(client.getChainId(), unknownToError) - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "getChainId" }) @@ -223,7 +226,6 @@ export function convertToSafeViemPublicClient( result: result.toString(), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -241,7 +243,8 @@ export function convertToSafeViemPublicClient( const startTime = Date.now() return ResultAsync.fromPromise(client.getTransactionCount(...args), unknownToError) - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "getTransactionCount" }) @@ -249,7 +252,6 @@ export function convertToSafeViemPublicClient( result: result.toString(), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -267,7 +269,8 @@ export function convertToSafeViemPublicClient( const startTime = Date.now() return ResultAsync.fromPromise(client.getFeeHistory(...args), unknownToError) - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "getFeeHistory" }) @@ -275,7 +278,6 @@ export function convertToSafeViemPublicClient( result: JSON.stringify(result, bigIntReplacer), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -322,7 +324,8 @@ export function convertToSafeViemWalletClient( const startTime = Date.now() return ResultAsync.fromPromise(client.sendRawTransaction(...args), unknownToError) - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "sendRawTransaction" }) @@ -330,7 +333,6 @@ export function convertToSafeViemWalletClient( result: result.toString(), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -365,7 +367,8 @@ export function convertToSafeViemWalletClient( "A viem update probably change the internal signing API."); return client.signTransaction(args) })() - .map((result) => { + .andThen(errorOnNullish) + .andTee((result) => { const duration = Date.now() - startTime if (safeClient.rpcResponseTimeHistogram) safeClient.rpcResponseTimeHistogram.record(duration, { method: "signTransaction" }) @@ -373,7 +376,6 @@ export function convertToSafeViemWalletClient( result: JSON.stringify(result, bigIntReplacer), }) span?.end() - return result }) .mapErr((error) => { if (safeClient.rpcErrorCounter) { @@ -387,3 +389,14 @@ export function convertToSafeViemWalletClient( return safeClient } + +class NullishResultError extends Error {} + +// Note that undefined error results are not hypothetical: we have observed them with our ProxyServer testing util when +// the connection is shut down. This should now never occurs, but we used ProxyServer shut down the connection when +// instructed to not answer — now we wait for 1 minute (but then shut down the connection). Viem just doesn't seem to +// handle this case gracefully? + +function errorOnNullish(result: T): ResultAsync { + return isNullish(result) ? errAsync(new NullishResultError("nullish result")) : okAsync(result) +} diff --git a/packages/txm/package.json b/packages/txm/package.json index a6e14268e0..cc3a52f7f6 100644 --- a/packages/txm/package.json +++ b/packages/txm/package.json @@ -8,12 +8,12 @@ "types": "./dist/index.es.d.ts", "exports": { ".": { - "default": "./dist/index.es.js", - "types": "./dist/index.es.d.ts" + "types": "./dist/index.es.d.ts", + "default": "./dist/index.es.js" }, "./migrate": { - "default": "./dist/migrate.es.js", - "types": "./dist/migrate.es.d.ts" + "types": "./dist/migrate.es.d.ts", + "default": "./dist/migrate.es.js" } }, "dependencies": { diff --git a/packages/txm/test/txm.test.ts b/packages/txm/test/txm.test.ts index 93f80ee87a..6f2f358052 100644 --- a/packages/txm/test/txm.test.ts +++ b/packages/txm/test/txm.test.ts @@ -32,6 +32,10 @@ import { deployMockContracts } from "./utils/contracts" import { assertIsDefined, assertIsOk, assertReceiptReverted, assertReceiptSuccess } from "./utils/customAsserts" import { cleanDB, getPersistedTransaction } from "./utils/db" +// Logs are disabled by default, uncomment for log info. +// import { logger } from "../lib/utils/logger" +// logger.setLogLevel(LogLevel.INFO) + const retryManager = new TestRetryManager() const txmConfig: TransactionManagerConfig = { @@ -200,9 +204,10 @@ test("onTransactionStatusChanged hook works correctly", async () => { transactionQueue.push(transaction) + let counter = 0 const cleanHook = await txm.addHook(TxmHookType.TransactionStatusChanged, (transactionInHook) => { hookTriggered = true - expect(transactionInHook.status).toBe(TransactionStatus.Success) + expect(transactionInHook.status).toBe(counter++ === 0 ? TransactionStatus.Pending : TransactionStatus.Success) expect(transactionInHook.intentId).toBe(transaction.intentId) }) @@ -260,7 +265,7 @@ test("TransactionSaveFailed hook works correctly", async () => { const cleanHook = await txm.addHook(TxmHookType.TransactionSaveFailed, (transactionInHook) => { hookTriggered = true - expect(transactionInHook.status).toBe(TransactionStatus.Pending) + expect(transactionInHook.status).toBe(TransactionStatus.NotAttempted) expect(transactionInHook.intentId).toBe(transaction.intentId) }) @@ -395,6 +400,19 @@ test("Transaction retried", async () => { expect(transactionPending.status).toBe(TransactionStatus.Pending) + // TODO If we fix ProxyServer to hang on ProxyBehaviour.NotAnswer, the previous line needs to be replaced by the + // code below. Other tests also need to be fixed, I only attempted to fix this one and gave up halfway, + // as this isn't really a priority. + + // // Will still be NotAttempted as the submit call hasn't timed out yet. + // expect(transactionPending.status).toBe(TransactionStatus.NotAttempted) + // // Necessary so that the liveness monitor doesn't get tripped up. + // const interval = setInterval(() => { + // mineBlock() + // }, 2000) + // await sleep(8_000) // ensures the initial attempt times out & RPC recovers healthy status + // clearInterval(interval) + await mineBlock() const transactionSuccessResult = await txm.getTransaction(transaction.intentId) diff --git a/support/testing/ProxyServer.ts b/support/testing/ProxyServer.ts index a37e267dc0..c2b9d3012f 100644 --- a/support/testing/ProxyServer.ts +++ b/support/testing/ProxyServer.ts @@ -1,6 +1,6 @@ import type { Server } from "node:http" import type { Http2SecureServer, Http2Server } from "node:http2" -import { type Logger, type TaggedLogger, stringify } from "@happy.tech/common" +import { type Logger, type TaggedLogger, sleep, stringify } from "@happy.tech/common" import { waitForCondition } from "@happy.tech/wallet-common" import { serve } from "@hono/node-server" import { createNodeWebSocket } from "@hono/node-ws" @@ -185,7 +185,20 @@ export class ProxyServer { const body = await c.req.json() const response = this.#getNextBehavior(body.method) - if (response === ProxyBehavior.NotAnswer) return + if (response === ProxyBehavior.NotAnswer) { + // There is no way to keep the connection hanging after returning, so the best we can do is sleep a + // really long time that should exceed all timeouts. + // TODO Commented out because the TXM test suite just doesn't handle this correctly at the moment. + // This causes the liveness monitor to go off the rails, etc... + // await sleep(60_000) + + // Mark context as handled to prevent Hono finalization warning. + c.finalized = true + // Returning here will shut down the connection without sending an answer — this causes Viem to + // seemingly return undefined results (??). + // TODO add connection shutdown as its own behaviour + return + } if (response === ProxyBehavior.Fail) return c.json({ error: "Proxy error" }, 500) // ProxyBehavior.forward