@imbingox/acex 0.3.0-beta.1 → 0.3.0-beta.3

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.
@@ -1,9 +1,11 @@
1
1
  import { BinanceMarketAdapter } from "../adapters/binance/adapter.ts";
2
2
  import { BinancePrivateAdapter } from "../adapters/binance/private-adapter.ts";
3
+ import { JuplendPrivateAdapter } from "../adapters/juplend/private-adapter.ts";
3
4
  import type {
4
5
  CancelAllOrdersRequest,
5
6
  CancelOrderRequest,
6
7
  CreateOrderRequest,
8
+ MarketAdapter,
7
9
  PrivateUserDataAdapter,
8
10
  RawOrderUpdate,
9
11
  } from "../adapters/types.ts";
@@ -33,6 +35,8 @@ import type {
33
35
  RegisterAccountInput,
34
36
  RegisterAccountResult,
35
37
  StopOptions,
38
+ Venue,
39
+ VenueCapabilities,
36
40
  } from "../types/index.ts";
37
41
  import {
38
42
  type ClientContext,
@@ -43,6 +47,10 @@ import {
43
47
  type RegisteredAccountRecord,
44
48
  } from "./context.ts";
45
49
  import { PrivateSubscriptionCoordinator } from "./private-subscription-coordinator.ts";
50
+ import {
51
+ getVenueCapabilitiesSnapshot,
52
+ listVenueCapabilitiesSnapshots,
53
+ } from "./venue-capabilities.ts";
46
54
 
47
55
  const activeClients = new Set<AcexClientImpl>();
48
56
 
@@ -87,14 +95,22 @@ export class AcexClientImpl implements AcexClient, ClientContext {
87
95
  private readonly marketManager: MarketManagerImpl;
88
96
  private readonly accountManager: AccountManagerImpl;
89
97
  private readonly orderManager: OrderManagerImpl;
90
- private readonly privateAdapter: PrivateUserDataAdapter;
98
+ private readonly marketAdapters: Map<Venue, MarketAdapter>;
99
+ private readonly privateAdapters: Map<Venue, PrivateUserDataAdapter>;
91
100
  private readonly privateCoordinator: PrivateSubscriptionCoordinator;
92
101
 
93
102
  constructor(options: CreateClientOptions = {}) {
94
103
  activeClients.add(this);
95
104
 
96
105
  const marketAdapter = new BinanceMarketAdapter();
97
- this.privateAdapter = new BinancePrivateAdapter();
106
+ this.marketAdapters = new Map([[marketAdapter.venue, marketAdapter]]);
107
+ const privateAdapters = [
108
+ new BinancePrivateAdapter(),
109
+ new JuplendPrivateAdapter(),
110
+ ];
111
+ this.privateAdapters = new Map(
112
+ privateAdapters.map((adapter) => [adapter.venue, adapter]),
113
+ );
98
114
 
99
115
  this.marketManager = new MarketManagerImpl(this, marketAdapter, {
100
116
  initialL1TimeoutMs: options.market?.l1InitialMessageTimeoutMs,
@@ -106,7 +122,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
106
122
  this.orderManager = new OrderManagerImpl(this);
107
123
  this.privateCoordinator = new PrivateSubscriptionCoordinator(
108
124
  this,
109
- this.privateAdapter,
125
+ privateAdapters,
110
126
  this.accountManager as PrivateAccountDataConsumer,
111
127
  this.orderManager as PrivateOrderDataConsumer,
112
128
  options.account,
@@ -134,6 +150,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
134
150
  };
135
151
  }
136
152
 
153
+ getVenueCapabilities(venue: Venue): VenueCapabilities {
154
+ return getVenueCapabilitiesSnapshot(venue, {
155
+ marketAdapters: this.marketAdapters,
156
+ privateAdapters: this.privateAdapters,
157
+ });
158
+ }
159
+
160
+ listVenueCapabilities(): VenueCapabilities[] {
161
+ return listVenueCapabilitiesSnapshots({
162
+ marketAdapters: this.marketAdapters,
163
+ privateAdapters: this.privateAdapters,
164
+ });
165
+ }
166
+
137
167
  async registerAccount(
138
168
  input: RegisterAccountInput,
139
169
  ): Promise<RegisterAccountResult> {
@@ -141,20 +171,20 @@ export class AcexClientImpl implements AcexClient, ClientContext {
141
171
  throw this.createError(
142
172
  "ACCOUNT_ALREADY_EXISTS",
143
173
  `Account already exists: ${input.accountId}`,
144
- { accountId: input.accountId, exchange: input.exchange },
174
+ { accountId: input.accountId, venue: input.venue },
145
175
  );
146
176
  }
147
177
 
148
178
  this.registeredAccounts.set(input.accountId, {
149
179
  accountId: input.accountId,
150
- exchange: input.exchange,
180
+ venue: input.venue,
151
181
  credentials: input.credentials,
152
- options: input.options,
182
+ options: input.options as Record<string, unknown> | undefined,
153
183
  });
154
184
 
155
185
  return {
156
186
  accountId: input.accountId,
157
- exchange: input.exchange,
187
+ venue: input.venue,
158
188
  };
159
189
  }
160
190
 
@@ -177,8 +207,8 @@ export class AcexClientImpl implements AcexClient, ClientContext {
177
207
  return;
178
208
  }
179
209
 
180
- this.accountManager.onCredentialsUpdated(accountId, account.exchange);
181
- this.orderManager.onCredentialsUpdated(accountId, account.exchange);
210
+ this.accountManager.onCredentialsUpdated(accountId, account.venue);
211
+ this.orderManager.onCredentialsUpdated(accountId, account.venue);
182
212
  this.privateCoordinator.onCredentialsUpdated(accountId);
183
213
  }
184
214
 
@@ -262,14 +292,14 @@ export class AcexClientImpl implements AcexClient, ClientContext {
262
292
 
263
293
  ensurePrivateCredentials(accountId: string): void {
264
294
  const account = this.getRegisteredAccount(accountId);
265
- if (hasPrivateCredentials(account.credentials)) {
295
+ if (hasPrivateCredentials(account.credentials, account.venue)) {
266
296
  return;
267
297
  }
268
298
 
269
299
  throw this.createError(
270
300
  "CREDENTIALS_MISSING",
271
301
  `Account credentials are required for private subscriptions: ${accountId}`,
272
- { accountId, exchange: account.exchange },
302
+ { accountId, venue: account.venue },
273
303
  );
274
304
  }
275
305
 
@@ -303,7 +333,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
303
333
  positionSide: input.positionSide,
304
334
  };
305
335
 
306
- return this.privateAdapter.createOrder(
336
+ return this.getPrivateAdapter(account.venue).createOrder(
307
337
  account.credentials ?? {},
308
338
  request,
309
339
  account.options,
@@ -318,7 +348,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
318
348
  clientOrderId: input.clientOrderId,
319
349
  };
320
350
 
321
- return this.privateAdapter.cancelOrder(
351
+ return this.getPrivateAdapter(account.venue).cancelOrder(
322
352
  account.credentials ?? {},
323
353
  request,
324
354
  account.options,
@@ -331,7 +361,7 @@ export class AcexClientImpl implements AcexClient, ClientContext {
331
361
  symbol: input.symbol,
332
362
  };
333
363
 
334
- return this.privateAdapter.cancelAllOrders(
364
+ return this.getPrivateAdapter(account.venue).cancelAllOrders(
335
365
  account.credentials ?? {},
336
366
  request,
337
367
  account.options,
@@ -390,22 +420,36 @@ export class AcexClientImpl implements AcexClient, ClientContext {
390
420
 
391
421
  private getPrivateCommandAccount(accountId: string): RegisteredAccountRecord {
392
422
  const account = this.getRegisteredAccount(accountId);
393
- if (account.exchange !== this.privateAdapter.exchange) {
423
+ const adapter = this.getPrivateAdapter(account.venue);
424
+ if (adapter.venue === "juplend") {
394
425
  throw this.createError(
395
- "EXCHANGE_NOT_SUPPORTED",
396
- `Exchange is not supported yet: ${account.exchange}`,
397
- { accountId, exchange: account.exchange },
426
+ "VENUE_NOT_SUPPORTED",
427
+ `Venue does not support private order commands: ${account.venue}`,
428
+ { accountId, venue: account.venue },
398
429
  );
399
430
  }
400
431
 
401
- if (!hasPrivateCredentials(account.credentials)) {
432
+ if (!hasPrivateCredentials(account.credentials, account.venue)) {
402
433
  throw this.createError(
403
434
  "CREDENTIALS_MISSING",
404
435
  `Account credentials are required for private order commands: ${accountId}`,
405
- { accountId, exchange: account.exchange },
436
+ { accountId, venue: account.venue },
406
437
  );
407
438
  }
408
439
 
409
440
  return account;
410
441
  }
442
+
443
+ private getPrivateAdapter(venue: Venue): PrivateUserDataAdapter {
444
+ const adapter = this.privateAdapters.get(venue);
445
+ if (!adapter) {
446
+ throw this.createError(
447
+ "VENUE_NOT_SUPPORTED",
448
+ `Venue is not supported yet: ${venue}`,
449
+ { venue },
450
+ );
451
+ }
452
+
453
+ return adapter;
454
+ }
411
455
  }
@@ -0,0 +1,109 @@
1
+ import type {
2
+ MarketAdapter,
3
+ PrivateUserDataAdapter,
4
+ } from "../adapters/types.ts";
5
+ import type {
6
+ VenueAccountCapabilities,
7
+ VenueCapabilities,
8
+ VenueMarketCapabilities,
9
+ VenueOrderCapabilities,
10
+ } from "../types/client.ts";
11
+ import { SUPPORTED_VENUES, type Venue } from "../types/shared.ts";
12
+
13
+ const unsupportedAccount: VenueAccountCapabilities = {
14
+ register: "unsupported",
15
+ snapshot: "unsupported",
16
+ updates: "unsupported",
17
+ balances: "unsupported",
18
+ positions: "unsupported",
19
+ risk: "unsupported",
20
+ lending: "unsupported",
21
+ credentialsRequired: false,
22
+ };
23
+
24
+ const unsupportedOrder: Omit<VenueOrderCapabilities, "reason"> = {
25
+ supported: false,
26
+ openOrders: "unsupported",
27
+ updates: "unsupported",
28
+ create: "unsupported",
29
+ cancel: "unsupported",
30
+ cancelAll: "unsupported",
31
+ orderTypes: [],
32
+ timeInForce: [],
33
+ postOnly: false,
34
+ reduceOnly: false,
35
+ positionSide: "unsupported",
36
+ clientOrderId: false,
37
+ };
38
+
39
+ const typeOnlyNotes = [
40
+ "Venue is declared in public types but has no runtime adapter yet.",
41
+ ];
42
+
43
+ const unsupportedMarket: VenueMarketCapabilities = {
44
+ catalog: "unsupported",
45
+ l1Book: "unsupported",
46
+ fundingRate: "unsupported",
47
+ marketTypes: [],
48
+ };
49
+
50
+ export interface VenueCapabilityAdapterRegistry {
51
+ marketAdapters: ReadonlyMap<Venue, MarketAdapter>;
52
+ privateAdapters: ReadonlyMap<Venue, PrivateUserDataAdapter>;
53
+ }
54
+
55
+ export function getVenueCapabilitiesSnapshot(
56
+ venue: Venue,
57
+ registry: VenueCapabilityAdapterRegistry,
58
+ ): VenueCapabilities {
59
+ const marketAdapter = registry.marketAdapters.get(venue);
60
+ const privateAdapter = registry.privateAdapters.get(venue);
61
+
62
+ return cloneVenueCapabilities({
63
+ venue,
64
+ runtimeStatus: marketAdapter || privateAdapter ? "available" : "type_only",
65
+ readOnly: privateAdapter?.readOnly ?? false,
66
+ notes:
67
+ privateAdapter?.notes ??
68
+ (marketAdapter
69
+ ? [
70
+ "Capabilities describe the current SDK runtime, not the venue's full API surface.",
71
+ ]
72
+ : typeOnlyNotes),
73
+ market: marketAdapter?.marketCapabilities ?? unsupportedMarket,
74
+ account: privateAdapter?.accountCapabilities ?? unsupportedAccount,
75
+ order: privateAdapter?.orderCapabilities ?? {
76
+ ...unsupportedOrder,
77
+ reason: "not_implemented",
78
+ },
79
+ });
80
+ }
81
+
82
+ export function listVenueCapabilitiesSnapshots(
83
+ registry: VenueCapabilityAdapterRegistry,
84
+ ): VenueCapabilities[] {
85
+ return SUPPORTED_VENUES.map((venue) =>
86
+ getVenueCapabilitiesSnapshot(venue, registry),
87
+ );
88
+ }
89
+
90
+ function cloneVenueCapabilities(
91
+ capabilities: VenueCapabilities,
92
+ ): VenueCapabilities {
93
+ return {
94
+ ...capabilities,
95
+ notes: [...capabilities.notes],
96
+ market: {
97
+ ...capabilities.market,
98
+ marketTypes: [...capabilities.market.marketTypes],
99
+ },
100
+ account: {
101
+ ...capabilities.account,
102
+ },
103
+ order: {
104
+ ...capabilities.order,
105
+ orderTypes: [...capabilities.order.orderTypes],
106
+ timeInForce: [...capabilities.order.timeInForce],
107
+ },
108
+ };
109
+ }
package/src/errors.ts CHANGED
@@ -4,7 +4,7 @@ export type AcexErrorCode =
4
4
  | "ACCOUNT_NOT_FOUND"
5
5
  | "CLIENT_NOT_STARTED"
6
6
  | "CREDENTIALS_MISSING"
7
- | "EXCHANGE_NOT_SUPPORTED"
7
+ | "VENUE_NOT_SUPPORTED"
8
8
  | "MARKET_CATALOG_LOAD_FAILED"
9
9
  | "MARKET_INACTIVE"
10
10
  | "MARKET_FUNDING_RATE_UNSUPPORTED"
@@ -1,21 +1,21 @@
1
1
  import type {
2
2
  AccountEventFilter,
3
- Exchange,
4
3
  HealthEvent,
5
4
  HealthEventFilter,
6
5
  MarketEventFilter,
7
6
  OrderEventFilter,
7
+ Venue,
8
8
  } from "../types/index.ts";
9
9
 
10
10
  export function matchesMarketFilter(
11
- event: { exchange: Exchange; symbol: string },
11
+ event: { venue: Venue; symbol: string },
12
12
  filter?: MarketEventFilter,
13
13
  ): boolean {
14
14
  if (!filter) {
15
15
  return true;
16
16
  }
17
17
 
18
- if (filter.exchange && event.exchange !== filter.exchange) {
18
+ if (filter.venue && event.venue !== filter.venue) {
19
19
  return false;
20
20
  }
21
21
 
@@ -27,7 +27,7 @@ export function matchesMarketFilter(
27
27
  }
28
28
 
29
29
  export function matchesAccountFilter(
30
- event: { accountId: string; exchange: Exchange; symbol?: string },
30
+ event: { accountId: string; venue: Venue; symbol?: string },
31
31
  filter?: AccountEventFilter,
32
32
  ): boolean {
33
33
  if (!filter) {
@@ -38,7 +38,7 @@ export function matchesAccountFilter(
38
38
  return false;
39
39
  }
40
40
 
41
- if (filter.exchange && event.exchange !== filter.exchange) {
41
+ if (filter.venue && event.venue !== filter.venue) {
42
42
  return false;
43
43
  }
44
44
 
@@ -50,7 +50,7 @@ export function matchesAccountFilter(
50
50
  }
51
51
 
52
52
  export function matchesOrderFilter(
53
- event: { accountId: string; exchange: Exchange; symbol?: string },
53
+ event: { accountId: string; venue: Venue; symbol?: string },
54
54
  filter?: OrderEventFilter,
55
55
  ): boolean {
56
56
  if (!filter) {
@@ -61,7 +61,7 @@ export function matchesOrderFilter(
61
61
  return false;
62
62
  }
63
63
 
64
- if (filter.exchange && event.exchange !== filter.exchange) {
64
+ if (filter.venue && event.venue !== filter.venue) {
65
65
  return false;
66
66
  }
67
67
 
@@ -95,8 +95,8 @@ export function matchesHealthFilter(
95
95
  }
96
96
  }
97
97
 
98
- if (filter.exchange) {
99
- if (!("exchange" in event) || event.exchange !== filter.exchange) {
98
+ if (filter.venue) {
99
+ if (!("venue" in event) || event.venue !== filter.venue) {
100
100
  return false;
101
101
  }
102
102
  }