From c3d1cd2c2713fd87027753d555857a5d2ced3952 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Tue, 30 Jun 2026 06:34:28 +0000 Subject: [PATCH] fix(ts-sdk): make hosted errors catchable by base class --- sdks/typescript/pmxt/hosted-errors.ts | 14 +++++++++++--- sdks/typescript/tests/hosted-error-mapping.test.ts | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/sdks/typescript/pmxt/hosted-errors.ts b/sdks/typescript/pmxt/hosted-errors.ts index 8fc13968..c0cc4592 100644 --- a/sdks/typescript/pmxt/hosted-errors.ts +++ b/sdks/typescript/pmxt/hosted-errors.ts @@ -5,9 +5,9 @@ * does not support multiple inheritance, so each hosted error class extends * the semantically-closest legacy parent (e.g. `InsufficientEscrowBalance` * extends `InsufficientFunds`) so existing `instanceof` checks continue to - * match. The `static isHostedError = true` flag and the {@link isHostedError} - * helper provide a structural alternative for callers that want to detect - * hosted-mode failures regardless of the concrete subclass. + * match. `HostedTradingError[Symbol.hasInstance]` uses the `static + * isHostedError = true` flag so callers can also catch any hosted-mode failure + * with `e instanceof HostedTradingError`. */ import { @@ -25,6 +25,14 @@ export class HostedTradingError extends PmxtError { readonly status: number; readonly detail: string; + static [Symbol.hasInstance](value: unknown): boolean { + if (this !== HostedTradingError) { + return Function.prototype[Symbol.hasInstance].call(this, value); + } + const ctor = (value as { constructor?: { isHostedError?: boolean } } | null)?.constructor; + return ctor != null && ctor.isHostedError === true; + } + constructor(status: number, detail: string) { super(detail); this.status = status; diff --git a/sdks/typescript/tests/hosted-error-mapping.test.ts b/sdks/typescript/tests/hosted-error-mapping.test.ts index 505273b4..6f952beb 100644 --- a/sdks/typescript/tests/hosted-error-mapping.test.ts +++ b/sdks/typescript/tests/hosted-error-mapping.test.ts @@ -114,6 +114,20 @@ describe("isHostedError flag", () => { expect(isHostedError(err)).toBe(true); }); + it.each([ + ["HostedTradingError", new HostedTradingError(500, "x")], + ["InvalidApiKey", new InvalidApiKey(401, "x")], + ["InsufficientEscrowBalance", new InsufficientEscrowBalance(403, "x")], + ["OrderSizeTooSmall", new OrderSizeTooSmall(422, "x")], + ["OutcomeNotFound", new OutcomeNotFound(404, "x")], + ["CatalogUnavailable", new CatalogUnavailable(503, "x")], + ["BuiltOrderExpired", new BuiltOrderExpired(410, "x")], + ["InvalidSignature", new InvalidSignature(422, "x")], + ["NoLiquidity", new NoLiquidity(422, "x")], + ])("%s is catchable as HostedTradingError", (_label, err) => { + expect(err).toBeInstanceOf(HostedTradingError); + }); + it("MissingWalletAddress is NOT a hosted error (local-only)", () => { expect(isHostedError(new MissingWalletAddress("x"))).toBe(false); });