@imbingox/acex 0.4.0-beta.7 → 0.4.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imbingox/acex",
3
- "version": "0.4.0-beta.7",
3
+ "version": "0.4.0-beta.9",
4
4
  "description": "Multi-exchange trading SDK for market data, account, and order management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -112,7 +112,9 @@ function inferContractType(
112
112
  contractType: string | undefined,
113
113
  deliveryDate: number | undefined,
114
114
  ): MarketType {
115
- if (contractType === "PERPETUAL") {
115
+ // Binance TradFi perpetuals expose a far-future deliveryDate, so the
116
+ // contractType is authoritative for perpetual classification.
117
+ if (contractType === "PERPETUAL" || contractType === "TRADIFI_PERPETUAL") {
116
118
  return "swap";
117
119
  }
118
120
 
@@ -1,5 +1,4 @@
1
1
  import BigNumber from "bignumber.js";
2
- import { AcexError } from "../../errors.ts";
3
2
  import {
4
3
  type HttpClientMessages,
5
4
  httpRequest,
@@ -734,28 +733,21 @@ export class JuplendPrivateAdapter implements PrivateUserDataAdapter {
734
733
  _credentials: AccountCredentials,
735
734
  _request: CreateOrderRequest,
736
735
  ): Promise<RawOrderUpdate> {
737
- throw new AcexError(
738
- "VENUE_NOT_SUPPORTED",
739
- "Juplend is read-only and does not support createOrder",
740
- );
736
+ throw new Error("Juplend is read-only and does not support createOrder");
741
737
  }
742
738
 
743
739
  cancelOrder(
744
740
  _credentials: AccountCredentials,
745
741
  _request: CancelOrderRequest,
746
742
  ): Promise<RawOrderUpdate> {
747
- throw new AcexError(
748
- "VENUE_NOT_SUPPORTED",
749
- "Juplend is read-only and does not support cancelOrder",
750
- );
743
+ throw new Error("Juplend is read-only and does not support cancelOrder");
751
744
  }
752
745
 
753
746
  cancelAllOrders(
754
747
  _credentials: AccountCredentials,
755
748
  _request: CancelAllOrdersRequest,
756
749
  ): Promise<RawOrderUpdate[]> {
757
- throw new AcexError(
758
- "VENUE_NOT_SUPPORTED",
750
+ throw new Error(
759
751
  "Juplend is read-only and does not support cancelAllOrders",
760
752
  );
761
753
  }
@@ -2,8 +2,12 @@ import type {
2
2
  PrivateUserDataAdapter,
3
3
  StreamHandle,
4
4
  } from "../adapters/types.ts";
5
- import { AcexError } from "../errors.ts";
6
- import { isTransportError, redactSecrets } from "../internal/http-client.ts";
5
+ import {
6
+ AcexError,
7
+ buildAcexErrorDetails,
8
+ formatAcexErrorMessage,
9
+ } from "../errors.ts";
10
+ import { isTransportError } from "../internal/http-client.ts";
7
11
  import type {
8
12
  AccountRuntimeOptions,
9
13
  PrivateRuntimeReason,
@@ -55,14 +59,6 @@ function transportReason(
55
59
  : fallback;
56
60
  }
57
61
 
58
- function bootstrapErrorDetail(error: unknown): string {
59
- if (!(error instanceof Error) || !error.message) {
60
- return "";
61
- }
62
-
63
- return ` (${redactSecrets(error.message)})`;
64
- }
65
-
66
62
  export class PrivateSubscriptionCoordinator {
67
63
  private readonly context: ClientContext;
68
64
  private readonly adapters: Map<Venue, PrivateUserDataAdapter>;
@@ -260,6 +256,7 @@ export class PrivateSubscriptionCoordinator {
260
256
  throw new AcexError(
261
257
  "VENUE_NOT_SUPPORTED",
262
258
  `Venue is not supported yet: ${account.venue}`,
259
+ { details: buildAcexErrorDetails({ venue: account.venue }) },
263
260
  );
264
261
  }
265
262
 
@@ -272,6 +269,7 @@ export class PrivateSubscriptionCoordinator {
272
269
  throw new AcexError(
273
270
  "VENUE_NOT_SUPPORTED",
274
271
  `Venue is not supported yet: ${venue}`,
272
+ { details: buildAcexErrorDetails({ venue }) },
275
273
  );
276
274
  }
277
275
 
@@ -503,6 +501,12 @@ export class PrivateSubscriptionCoordinator {
503
501
  throw new AcexError(
504
502
  "CREDENTIALS_MISSING",
505
503
  `Account credentials are required for private subscriptions: ${account.accountId}`,
504
+ {
505
+ details: buildAcexErrorDetails({
506
+ accountId: account.accountId,
507
+ venue: account.venue,
508
+ }),
509
+ },
506
510
  );
507
511
  }
508
512
 
@@ -716,10 +720,23 @@ export class PrivateSubscriptionCoordinator {
716
720
  ),
717
721
  },
718
722
  );
719
- const reason = bootstrapErrorDetail(error);
723
+ const details = buildAcexErrorDetails(
724
+ {
725
+ accountId: record.accountId,
726
+ venue: record.venue,
727
+ },
728
+ error,
729
+ );
720
730
  throw new AcexError(
721
731
  "ACCOUNT_BOOTSTRAP_FAILED",
722
- `Failed to bootstrap account data: ${record.accountId}${reason}`,
732
+ formatAcexErrorMessage(
733
+ `Failed to bootstrap account data: ${record.accountId}`,
734
+ details,
735
+ ),
736
+ {
737
+ cause: error,
738
+ details,
739
+ },
723
740
  );
724
741
  }
725
742
  }
@@ -766,9 +783,23 @@ export class PrivateSubscriptionCoordinator {
766
783
  reason: transportReason(error, "auth_failed"),
767
784
  },
768
785
  );
786
+ const details = buildAcexErrorDetails(
787
+ {
788
+ accountId: record.accountId,
789
+ venue: record.venue,
790
+ },
791
+ error,
792
+ );
769
793
  throw new AcexError(
770
794
  "ORDER_BOOTSTRAP_FAILED",
771
- `Failed to bootstrap order data: ${record.accountId}`,
795
+ formatAcexErrorMessage(
796
+ `Failed to bootstrap order data: ${record.accountId}`,
797
+ details,
798
+ ),
799
+ {
800
+ cause: error,
801
+ details,
802
+ },
772
803
  );
773
804
  }
774
805
  }
@@ -9,7 +9,11 @@ import type {
9
9
  PrivateUserDataAdapter,
10
10
  RawOrderUpdate,
11
11
  } from "../adapters/types.ts";
12
- import { AcexError, type AcexErrorCode } from "../errors.ts";
12
+ import {
13
+ AcexError,
14
+ type AcexErrorCode,
15
+ buildAcexErrorDetails,
16
+ } from "../errors.ts";
13
17
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
14
18
  import { matchesHealthFilter } from "../internal/filters.ts";
15
19
  import { ReactiveRateLimiter } from "../internal/rate-limiter.ts";
@@ -431,7 +435,9 @@ export class AcexClientImpl implements AcexClient, ClientContext {
431
435
  message: string,
432
436
  metadata?: Omit<AcexInternalError, "error" | "source" | "ts">,
433
437
  ): AcexError {
434
- const error = new AcexError(code, message);
438
+ const error = new AcexError(code, message, {
439
+ details: buildAcexErrorDetails(metadata),
440
+ });
435
441
  this.errorBus.publish({
436
442
  source: "client",
437
443
  ts: this.now(),
package/src/errors.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { isTransportError } from "./internal/http-client.ts";
2
+ import type { Venue } from "./types/shared.ts";
3
+
1
4
  export type AcexErrorCode =
2
5
  | "ACCOUNT_ALREADY_EXISTS"
3
6
  | "ACCOUNT_BOOTSTRAP_FAILED"
@@ -17,12 +20,162 @@ export type AcexErrorCode =
17
20
  | "ORDER_CREATE_FAILED"
18
21
  | "ORDER_INPUT_INVALID";
19
22
 
23
+ export type AcexErrorTransportKind =
24
+ | "timeout"
25
+ | "http"
26
+ | "network"
27
+ | "rate_limited"
28
+ | "parse";
29
+
30
+ export interface AcexVenueErrorDetails {
31
+ readonly code?: string;
32
+ readonly message?: string;
33
+ }
34
+
35
+ export interface AcexErrorTransportDetails {
36
+ readonly kind?: AcexErrorTransportKind;
37
+ readonly status?: number;
38
+ readonly statusText?: string;
39
+ readonly retryAfterMs?: number;
40
+ readonly retryable?: boolean;
41
+ readonly attempts?: number;
42
+ readonly rawBody?: string;
43
+ readonly url?: string;
44
+ }
45
+
46
+ export interface AcexErrorDetails {
47
+ readonly venue?: Venue;
48
+ readonly accountId?: string;
49
+ readonly symbol?: string;
50
+ readonly venueError?: AcexVenueErrorDetails;
51
+ readonly transport?: AcexErrorTransportDetails;
52
+ }
53
+
54
+ export interface AcexErrorOptions {
55
+ readonly cause?: unknown;
56
+ readonly details?: AcexErrorDetails;
57
+ }
58
+
20
59
  export class AcexError extends Error {
21
60
  readonly code: AcexErrorCode;
61
+ readonly details?: AcexErrorDetails;
62
+ override readonly cause?: unknown;
22
63
 
23
- constructor(code: AcexErrorCode, message: string) {
24
- super(message);
64
+ constructor(
65
+ code: AcexErrorCode,
66
+ message: string,
67
+ options: AcexErrorOptions = {},
68
+ ) {
69
+ super(message, { cause: options.cause });
25
70
  this.name = "AcexError";
26
71
  this.code = code;
72
+ this.details = options.details;
73
+ this.cause = options.cause;
74
+ }
75
+ }
76
+
77
+ export function buildAcexErrorDetails(
78
+ context?: Pick<AcexErrorDetails, "venue" | "accountId" | "symbol">,
79
+ cause?: unknown,
80
+ ): AcexErrorDetails | undefined {
81
+ const transport = buildTransportDetails(cause);
82
+ const venueError = parseVenueErrorDetails(transport?.rawBody);
83
+ const details: AcexErrorDetails = {
84
+ venue: context?.venue,
85
+ accountId: context?.accountId,
86
+ symbol: context?.symbol,
87
+ venueError,
88
+ transport,
89
+ };
90
+
91
+ return hasDetails(details) ? details : undefined;
92
+ }
93
+
94
+ export function formatAcexErrorMessage(
95
+ message: string,
96
+ details?: AcexErrorDetails,
97
+ ): string {
98
+ const venueErrorMessage = details?.venueError?.message?.trim();
99
+ if (!venueErrorMessage) {
100
+ return message;
101
+ }
102
+
103
+ const venue = details?.venue;
104
+ const venueLabel = venue ? formatVenueLabel(venue) : "Exchange";
105
+ return `${message} (${venueLabel} rejected: ${venueErrorMessage})`;
106
+ }
107
+
108
+ function buildTransportDetails(
109
+ cause: unknown,
110
+ ): AcexErrorTransportDetails | undefined {
111
+ if (!isTransportError(cause)) {
112
+ return undefined;
113
+ }
114
+
115
+ return pruneUndefined({
116
+ kind: cause.kind,
117
+ status: cause.status,
118
+ statusText: cause.statusText,
119
+ retryAfterMs: cause.retryAfterMs,
120
+ retryable: cause.retryable,
121
+ attempts: cause.attempts,
122
+ rawBody: cause.rawBody,
123
+ url: cause.url,
124
+ });
125
+ }
126
+
127
+ function parseVenueErrorDetails(
128
+ rawBody: string | undefined,
129
+ ): AcexVenueErrorDetails | undefined {
130
+ if (!rawBody) {
131
+ return undefined;
27
132
  }
133
+
134
+ let parsed: unknown;
135
+ try {
136
+ parsed = JSON.parse(rawBody);
137
+ } catch {
138
+ return undefined;
139
+ }
140
+
141
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
142
+ return undefined;
143
+ }
144
+
145
+ const record = parsed as Record<string, unknown>;
146
+ const code = record.code;
147
+ const message = record.msg ?? record.message;
148
+
149
+ if (
150
+ (typeof code !== "string" && typeof code !== "number") ||
151
+ typeof message !== "string" ||
152
+ message.trim() === ""
153
+ ) {
154
+ return undefined;
155
+ }
156
+
157
+ return {
158
+ code: String(code),
159
+ message,
160
+ };
161
+ }
162
+
163
+ function hasDetails(details: AcexErrorDetails): boolean {
164
+ return Boolean(
165
+ details.venue ||
166
+ details.accountId ||
167
+ details.symbol ||
168
+ details.venueError ||
169
+ details.transport,
170
+ );
171
+ }
172
+
173
+ function formatVenueLabel(venue: Venue): string {
174
+ return `${venue.charAt(0).toUpperCase()}${venue.slice(1)}`;
175
+ }
176
+
177
+ function pruneUndefined<T extends Record<string, unknown>>(input: T): T {
178
+ return Object.fromEntries(
179
+ Object.entries(input).filter(([, value]) => value !== undefined),
180
+ ) as T;
28
181
  }
package/src/index.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  export { BigNumber } from "bignumber.js";
2
2
  export { createClient } from "./client/create-client.ts";
3
- export type { AcexErrorCode } from "./errors.ts";
3
+ export type {
4
+ AcexErrorCode,
5
+ AcexErrorDetails,
6
+ AcexErrorOptions,
7
+ AcexErrorTransportDetails,
8
+ AcexErrorTransportKind,
9
+ AcexVenueErrorDetails,
10
+ } from "./errors.ts";
4
11
  export { AcexError } from "./errors.ts";
5
12
  export * from "./types/index.ts";
@@ -14,7 +14,11 @@ import type {
14
14
  HealthReporter,
15
15
  ManagerLifecycle,
16
16
  } from "../client/context.ts";
17
- import { AcexError } from "../errors.ts";
17
+ import {
18
+ AcexError,
19
+ buildAcexErrorDetails,
20
+ formatAcexErrorMessage,
21
+ } from "../errors.ts";
18
22
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
19
23
  import { toCanonical } from "../internal/decimal.ts";
20
24
  import { matchesMarketFilter } from "../internal/filters.ts";
@@ -606,9 +610,17 @@ export class MarketManagerImpl
606
610
  }
607
611
 
608
612
  private createCatalogLoadError(venue: Venue, error: unknown): AcexError {
613
+ const details = buildAcexErrorDetails({ venue }, error);
609
614
  const wrapped = new AcexError(
610
615
  "MARKET_CATALOG_LOAD_FAILED",
611
- `Failed to load market catalog from ${venue}`,
616
+ formatAcexErrorMessage(
617
+ `Failed to load market catalog from ${venue}`,
618
+ details,
619
+ ),
620
+ {
621
+ cause: error,
622
+ details,
623
+ },
612
624
  );
613
625
  this.context.publishRuntimeError(
614
626
  "adapter",
@@ -621,9 +633,17 @@ export class MarketManagerImpl
621
633
  }
622
634
 
623
635
  private createServerTimeFetchError(venue: Venue, error: unknown): AcexError {
636
+ const details = buildAcexErrorDetails({ venue }, error);
624
637
  const wrapped = new AcexError(
625
638
  "MARKET_SERVER_TIME_FETCH_FAILED",
626
- `Failed to fetch server time from ${venue}`,
639
+ formatAcexErrorMessage(
640
+ `Failed to fetch server time from ${venue}`,
641
+ details,
642
+ ),
643
+ {
644
+ cause: error,
645
+ details,
646
+ },
627
647
  );
628
648
  this.context.publishRuntimeError(
629
649
  "adapter",
@@ -758,11 +778,20 @@ export class MarketManagerImpl
758
778
 
759
779
  try {
760
780
  await record.l1BookStream.ready;
761
- } catch {
781
+ } catch (error) {
782
+ record.l1BookStream.close();
762
783
  record.l1BookStream = undefined;
784
+ const details = buildAcexErrorDetails(
785
+ { venue: market.venue, symbol: market.symbol },
786
+ error,
787
+ );
763
788
  const timeoutError = new AcexError(
764
789
  "MARKET_STREAM_TIMEOUT",
765
790
  `Timed out waiting for market data: ${market.symbol}`,
791
+ {
792
+ cause: error,
793
+ details,
794
+ },
766
795
  );
767
796
  this.context.publishRuntimeError("runtime", timeoutError, {
768
797
  venue: market.venue,
@@ -786,11 +815,20 @@ export class MarketManagerImpl
786
815
 
787
816
  try {
788
817
  await record.fundingRateStream.ready;
789
- } catch {
818
+ } catch (error) {
819
+ record.fundingRateStream.close();
790
820
  record.fundingRateStream = undefined;
821
+ const details = buildAcexErrorDetails(
822
+ { venue: market.venue, symbol: market.symbol },
823
+ error,
824
+ );
791
825
  const timeoutError = new AcexError(
792
826
  "MARKET_STREAM_TIMEOUT",
793
827
  `Timed out waiting for market data: ${market.symbol}`,
828
+ {
829
+ cause: error,
830
+ details,
831
+ },
794
832
  );
795
833
  this.context.publishRuntimeError("runtime", timeoutError, {
796
834
  venue: market.venue,
@@ -1189,7 +1227,9 @@ export class MarketManagerImpl
1189
1227
  metadata?: { venue?: Venue; symbol?: string },
1190
1228
  source: "market" | "client" = "market",
1191
1229
  ): AcexError {
1192
- const error = new AcexError(code, message);
1230
+ const error = new AcexError(code, message, {
1231
+ details: buildAcexErrorDetails(metadata),
1232
+ });
1193
1233
  this.context.publishRuntimeError(source, error, metadata);
1194
1234
  return error;
1195
1235
  }
@@ -8,7 +8,11 @@ import type {
8
8
  PrivateOrderDataConsumer,
9
9
  PrivateSubscriptionState,
10
10
  } from "../client/context.ts";
11
- import { AcexError } from "../errors.ts";
11
+ import {
12
+ AcexError,
13
+ buildAcexErrorDetails,
14
+ formatAcexErrorMessage,
15
+ } from "../errors.ts";
12
16
  import { AsyncEventBus } from "../internal/async-event-bus.ts";
13
17
  import { toCanonical } from "../internal/decimal.ts";
14
18
  import { matchesOrderFilter } from "../internal/filters.ts";
@@ -656,7 +660,8 @@ export class OrderManagerImpl
656
660
  symbol?: string;
657
661
  },
658
662
  ): AcexError {
659
- const error = new AcexError(code, message);
663
+ const details = buildAcexErrorDetails(metadata);
664
+ const error = new AcexError(code, message, { details });
660
665
  this.context.publishRuntimeError("order", error, metadata);
661
666
  return error;
662
667
  }
@@ -683,6 +688,10 @@ export class OrderManagerImpl
683
688
  error instanceof Error ? error : new Error(message),
684
689
  metadata,
685
690
  );
686
- return new AcexError(code, message);
691
+ const details = buildAcexErrorDetails(metadata, error);
692
+ return new AcexError(code, formatAcexErrorMessage(message, details), {
693
+ cause: error,
694
+ details,
695
+ });
687
696
  }
688
697
  }