@0xmonaco/core 1.1.8 → 1.1.10

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/README.md CHANGED
@@ -349,6 +349,41 @@ interface MarketAPI extends BaseAPI {
349
349
  }
350
350
  ```
351
351
 
352
+ ### RFQ API
353
+
354
+ RFQ (block trades): a taker requests quotes for a size, makers quote privately, the taker accepts one side of the aggregated view, and the selected makers get a 250 ms last look (silence executes). Available to accounts RFQ is enabled for; see the [RFQ guide](https://docs.0xmonaco.com/developers/rfq).
355
+
356
+ ```typescript
357
+ interface RfqAPI extends BaseAPI {
358
+ getCapabilities(): Promise<GetRfqCapabilitiesResponse>;
359
+ // As taker
360
+ createRfq(params: CreateRfqParams): Promise<RfqStatus>;
361
+ getRfq(rfqId: string): Promise<RfqStatus>;
362
+ listRfqs(params?: ListRfqsParams): Promise<ListRfqsResponse>;
363
+ acceptRfq(rfqId: string, params: AcceptRfqParams): Promise<RfqStatus>;
364
+ cancelRfq(rfqId: string, params?: CancelRfqParams): Promise<RfqStatus>;
365
+ // As maker
366
+ upsertQuote(rfqId: string, params: UpsertRfqQuoteParams): Promise<RfqQuoteVersion>;
367
+ withdrawQuote(rfqId: string, params: WithdrawRfqQuoteParams): Promise<WithdrawRfqQuoteResponse>;
368
+ respondLastLook(rfqId: string, params: RespondRfqLastLookParams): Promise<RespondRfqLastLookResponse>;
369
+ }
370
+ ```
371
+
372
+ Pushes arrive on `sdk.ws.rfq(handler)` (authenticated), and public block prints on `sdk.ws.rfqTrades(tradingPairId, handler)`.
373
+
374
+ A taker waiting for quotes should follow its view on the `RfqView` pushes rather than poll `getRfq`: the view is pushed on every change, and every `getRfq` rebuilds it on the server. `waitForRfqView` does this, reading `getRfq` only when a push may have been missed (at the start, after a reconnect, while the socket is down, and at the timeout). It rejects with an `InvalidStateError` when no view matched by `timeoutMs` (default 10000) or the RFQ ended first.
375
+
376
+ ```typescript
377
+ import { waitForRfqView } from "@0xmonaco/core";
378
+
379
+ const rfq = await sdk.rfq.createRfq({ tradingPairId, product: "PERPETUAL", quantity: "25", leverage: "2" });
380
+ const rfqId = rfq.terms!.rfqId;
381
+ const { view } = await waitForRfqView(sdk, rfqId, (view) => view.ask?.availableQuantity === "25", {
382
+ initialView: rfq.view,
383
+ });
384
+ await sdk.rfq.acceptRfq(rfqId, { side: "BUY", viewVersion: view.viewVersion });
385
+ ```
386
+
352
387
  ### WebSocket Authentication
353
388
 
354
389
  The SDK provides three WebSocket clients with different authentication requirements:
@@ -18,7 +18,7 @@
18
18
  * const orders = await applicationsAPI.listApplicationOrders({ clientId: config.clientId, status: "FILLED" });
19
19
  * ```
20
20
  */
21
- import type { ApplicationConfigResponse, ApplicationsAPI, GetAppStatsParams, GetAppStatsResponse, ListAppBalancesParams, ListAppBalancesResponse, ListAppMovementsParams, ListAppMovementsResponse, ListAppOrdersParams, ListAppOrdersResponse, ListAppUsersParams, ListAppUsersResponse } from "@0xmonaco/types";
21
+ import type { ApplicationConfigResponse, ApplicationsAPI, GetAppStatsParams, GetAppStatsResponse, ListAppBalancesParams, ListAppBalancesResponse, ListAppMarginAccountsParams, ListAppMarginAccountsResponse, ListAppMovementsParams, ListAppMovementsResponse, ListAppOrdersParams, ListAppOrdersResponse, ListAppUsersParams, ListAppUsersResponse } from "@0xmonaco/types";
22
22
  import { BaseAPI } from "../base";
23
23
  export declare class ApplicationsAPIImpl extends BaseAPI implements ApplicationsAPI {
24
24
  /**
@@ -96,7 +96,11 @@ export declare class ApplicationsAPIImpl extends BaseAPI implements Applications
96
96
  */
97
97
  listApplicationMovements(params: ListAppMovementsParams): Promise<ListAppMovementsResponse>;
98
98
  /**
99
- * Lists user balances held within the application.
99
+ * Lists user wallet balances held within the application.
100
+ *
101
+ * Funds held in a margin account are not included, so a user funded through
102
+ * margin can show zero here. Read those users from
103
+ * `GET /api/v1/applications/margin-accounts` (`listApplicationMarginAccounts`).
100
104
  *
101
105
  * Public — the application is selected by `params.clientId`.
102
106
  *
@@ -113,6 +117,23 @@ export declare class ApplicationsAPIImpl extends BaseAPI implements Applications
113
117
  * @throws {APIError} When the request fails (404 for an unknown, inactive or hidden client ID, 429 when rate limited)
114
118
  */
115
119
  listApplicationBalances(params: ListAppBalancesParams): Promise<ListAppBalancesResponse>;
120
+ /**
121
+ * Lists the application's margin accounts, sub-accounts included.
122
+ *
123
+ * Margin collateral is not a wallet balance, so an application that funds its
124
+ * users through margin reads it here, not from `listApplicationBalances`.
125
+ * Figures are a persisted baseline repriced at the latest risk marks, so they
126
+ * can briefly lag the matching engine. Copy-trading accounts are not listed.
127
+ * Public — the application is selected by `params.clientId`.
128
+ *
129
+ * Cursor paged: omit `pageToken` or pass `""` to start, then follow
130
+ * `nextPageToken` until it comes back empty.
131
+ *
132
+ * @param params - Pagination and filter options
133
+ * @returns Promise resolving to a paginated list of margin accounts
134
+ * @throws {APIError} When the request fails (404 for an unknown, inactive or hidden client ID, 429 when rate limited)
135
+ */
136
+ listApplicationMarginAccounts(params: ListAppMarginAccountsParams): Promise<ListAppMarginAccountsResponse>;
116
137
  /**
117
138
  * Gets aggregate volume and fee stats for the application.
118
139
  *
@@ -18,7 +18,7 @@
18
18
  * const orders = await applicationsAPI.listApplicationOrders({ clientId: config.clientId, status: "FILLED" });
19
19
  * ```
20
20
  */
21
- import { ListAppBalancesSchema, ListAppMovementsSchema, ListAppOrdersSchema, ListAppUsersSchema, validate } from "@0xmonaco/types";
21
+ import { ListAppBalancesSchema, ListAppMarginAccountsSchema, ListAppMovementsSchema, ListAppOrdersSchema, ListAppUsersSchema, validate } from "@0xmonaco/types";
22
22
  import { BaseAPI } from "../base";
23
23
  import { perpRoutes } from "../perp/routes";
24
24
  export class ApplicationsAPIImpl extends BaseAPI {
@@ -142,7 +142,11 @@ export class ApplicationsAPIImpl extends BaseAPI {
142
142
  }));
143
143
  }
144
144
  /**
145
- * Lists user balances held within the application.
145
+ * Lists user wallet balances held within the application.
146
+ *
147
+ * Funds held in a margin account are not included, so a user funded through
148
+ * margin can show zero here. Read those users from
149
+ * `GET /api/v1/applications/margin-accounts` (`listApplicationMarginAccounts`).
146
150
  *
147
151
  * Public — the application is selected by `params.clientId`.
148
152
  *
@@ -168,6 +172,31 @@ export class ApplicationsAPIImpl extends BaseAPI {
168
172
  assetId: params.assetId,
169
173
  }));
170
174
  }
175
+ /**
176
+ * Lists the application's margin accounts, sub-accounts included.
177
+ *
178
+ * Margin collateral is not a wallet balance, so an application that funds its
179
+ * users through margin reads it here, not from `listApplicationBalances`.
180
+ * Figures are a persisted baseline repriced at the latest risk marks, so they
181
+ * can briefly lag the matching engine. Copy-trading accounts are not listed.
182
+ * Public — the application is selected by `params.clientId`.
183
+ *
184
+ * Cursor paged: omit `pageToken` or pass `""` to start, then follow
185
+ * `nextPageToken` until it comes back empty.
186
+ *
187
+ * @param params - Pagination and filter options
188
+ * @returns Promise resolving to a paginated list of margin accounts
189
+ * @throws {APIError} When the request fails (404 for an unknown, inactive or hidden client ID, 429 when rate limited)
190
+ */
191
+ async listApplicationMarginAccounts(params) {
192
+ validate(ListAppMarginAccountsSchema, params);
193
+ return await this.makePublicRequest(perpRoutes.applications.marginAccounts({
194
+ clientId: params.clientId,
195
+ address: params.address,
196
+ pageSize: params.pageSize,
197
+ pageToken: params.pageToken,
198
+ }));
199
+ }
171
200
  /**
172
201
  * Gets aggregate volume and fee stats for the application.
173
202
  *
@@ -246,6 +246,12 @@ export declare const perpRoutes: {
246
246
  userId?: string;
247
247
  assetId?: string;
248
248
  }) => string;
249
+ readonly marginAccounts: (params?: {
250
+ clientId?: string;
251
+ address?: string;
252
+ pageSize?: number;
253
+ pageToken?: string;
254
+ }) => string;
249
255
  readonly stats: (params?: {
250
256
  clientId?: string;
251
257
  since?: string;
@@ -102,6 +102,7 @@ export const perpRoutes = {
102
102
  users: (params) => withQuery(`${API_V1}/applications/users`, params),
103
103
  movements: (params) => withQuery(`${API_V1}/applications/movements`, params),
104
104
  balances: (params) => withQuery(`${API_V1}/applications/balances`, params),
105
+ marginAccounts: (params) => withQuery(`${API_V1}/applications/margin-accounts`, params),
105
106
  stats: (params) => withQuery(`${API_V1}/applications/stats`, params),
106
107
  },
107
108
  subAccounts: {
@@ -14,18 +14,23 @@
14
14
  * the gateway rejects unknown fields and stores a maker's request exactly as
15
15
  * signed.
16
16
  *
17
+ * A taker waiting for quotes follows its view on the `rfq` WebSocket
18
+ * channel (`waitForRfqView`) rather than polling `getRfq`: the view is
19
+ * pushed on every change, and every `getRfq` rebuilds it on the server.
20
+ *
17
21
  * @example
18
22
  * ```typescript
19
23
  * // Taker
20
24
  * const rfq = await sdk.rfq.createRfq({ tradingPairId, product: "PERPETUAL", quantity: "25" });
21
- * const status = await sdk.rfq.getRfq(rfq.terms!.rfqId);
22
- * await sdk.rfq.acceptRfq(rfq.terms!.rfqId, { side: "BUY", viewVersion: status.view!.viewVersion });
25
+ * const rfqId = rfq.terms!.rfqId;
26
+ * const { view } = await waitForRfqView(sdk, rfqId, (view) => view.ask?.availableQuantity === "25", { initialView: rfq.view });
27
+ * await sdk.rfq.acceptRfq(rfqId, { side: "BUY", viewVersion: view.viewVersion });
23
28
  *
24
29
  * // Maker
25
30
  * const quote = await sdk.rfq.upsertQuote(rfqId, { expectedVersion: 0, ask: { price: "65001", capacity: "10" } });
26
31
  * ```
27
32
  */
28
- import type { AcceptRfqParams, CancelRfqParams, CreateRfqParams, GetRfqCapabilitiesResponse, RespondRfqLastLookParams, RespondRfqLastLookResponse, RfqAPI, RfqQuoteVersion, RfqStatus, UpsertRfqQuoteParams, WithdrawRfqQuoteParams, WithdrawRfqQuoteResponse } from "@0xmonaco/types";
33
+ import type { AcceptRfqParams, CancelRfqParams, CreateRfqParams, GetRfqCapabilitiesResponse, ListRfqsParams, ListRfqsResponse, RespondRfqLastLookParams, RespondRfqLastLookResponse, RfqAPI, RfqQuoteVersion, RfqStatus, UpsertRfqQuoteParams, WithdrawRfqQuoteParams, WithdrawRfqQuoteResponse } from "@0xmonaco/types";
29
34
  import { BaseAPI } from "../base";
30
35
  export declare class RfqAPIImpl extends BaseAPI implements RfqAPI {
31
36
  /** Your RFQ limits and the markets RFQ is offered on. */
@@ -41,9 +46,16 @@ export declare class RfqAPIImpl extends BaseAPI implements RfqAPI {
41
46
  /**
42
47
  * An RFQ's authoritative status. As taker: the current `view` and, once
43
48
  * executed, the `outcome`. As maker: your latest quote, `ownLatestVersion`
44
- * and, once selected, `ownSelection` for last look.
49
+ * and, once selected, `ownSelection` for last look. To wait for quotes,
50
+ * follow the `RfqView` pushes (`waitForRfqView`) instead of polling.
45
51
  */
46
52
  getRfq(rfqId: string): Promise<RfqStatus>;
53
+ /**
54
+ * Your RFQs as the taker, newest first, in every state (those that filled
55
+ * nothing included). Each is as `getRfq` gives it, without the live `view`.
56
+ * Follow `nextPageToken` until it is empty.
57
+ */
58
+ listRfqs(params?: ListRfqsParams): Promise<ListRfqsResponse>;
47
59
  /**
48
60
  * Accept one side of the view you last saw. Binding: the selected makers'
49
61
  * last look follows, and silence executes. A newer view refuses an older
@@ -14,18 +14,23 @@
14
14
  * the gateway rejects unknown fields and stores a maker's request exactly as
15
15
  * signed.
16
16
  *
17
+ * A taker waiting for quotes follows its view on the `rfq` WebSocket
18
+ * channel (`waitForRfqView`) rather than polling `getRfq`: the view is
19
+ * pushed on every change, and every `getRfq` rebuilds it on the server.
20
+ *
17
21
  * @example
18
22
  * ```typescript
19
23
  * // Taker
20
24
  * const rfq = await sdk.rfq.createRfq({ tradingPairId, product: "PERPETUAL", quantity: "25" });
21
- * const status = await sdk.rfq.getRfq(rfq.terms!.rfqId);
22
- * await sdk.rfq.acceptRfq(rfq.terms!.rfqId, { side: "BUY", viewVersion: status.view!.viewVersion });
25
+ * const rfqId = rfq.terms!.rfqId;
26
+ * const { view } = await waitForRfqView(sdk, rfqId, (view) => view.ask?.availableQuantity === "25", { initialView: rfq.view });
27
+ * await sdk.rfq.acceptRfq(rfqId, { side: "BUY", viewVersion: view.viewVersion });
23
28
  *
24
29
  * // Maker
25
30
  * const quote = await sdk.rfq.upsertQuote(rfqId, { expectedVersion: 0, ask: { price: "65001", capacity: "10" } });
26
31
  * ```
27
32
  */
28
- import { AcceptRfqSchema, CancelRfqSchema, CreateRfqSchema, RespondRfqLastLookSchema, UpsertRfqQuoteSchema, UUIDSchema, validate, WithdrawRfqQuoteSchema, } from "@0xmonaco/types";
33
+ import { AcceptRfqSchema, CancelRfqSchema, CreateRfqSchema, ListRfqsSchema, RespondRfqLastLookSchema, UpsertRfqQuoteSchema, UUIDSchema, validate, WithdrawRfqQuoteSchema, } from "@0xmonaco/types";
29
34
  import { BaseAPI } from "../base";
30
35
  const BASE = "/api/v1/rfqs";
31
36
  /** The RFQ path for a validated RFQ id. */
@@ -62,11 +67,27 @@ export class RfqAPIImpl extends BaseAPI {
62
67
  /**
63
68
  * An RFQ's authoritative status. As taker: the current `view` and, once
64
69
  * executed, the `outcome`. As maker: your latest quote, `ownLatestVersion`
65
- * and, once selected, `ownSelection` for last look.
70
+ * and, once selected, `ownSelection` for last look. To wait for quotes,
71
+ * follow the `RfqView` pushes (`waitForRfqView`) instead of polling.
66
72
  */
67
73
  async getRfq(rfqId) {
68
74
  return await this.makeAuthenticatedRequest(rfqPath(rfqId), { method: "GET" });
69
75
  }
76
+ /**
77
+ * Your RFQs as the taker, newest first, in every state (those that filled
78
+ * nothing included). Each is as `getRfq` gives it, without the live `view`.
79
+ * Follow `nextPageToken` until it is empty.
80
+ */
81
+ async listRfqs(params) {
82
+ const { pageSize, pageToken } = validate(ListRfqsSchema, params ?? {});
83
+ const searchParams = new URLSearchParams();
84
+ if (pageSize !== undefined)
85
+ searchParams.append("pageSize", pageSize.toString());
86
+ if (pageToken !== undefined)
87
+ searchParams.append("pageToken", pageToken);
88
+ const query = searchParams.toString();
89
+ return await this.makeAuthenticatedRequest(query ? `${BASE}?${query}` : BASE, { method: "GET" });
90
+ }
70
91
  /**
71
92
  * Accept one side of the view you last saw. Binding: the selected makers'
72
93
  * last look follows, and silence executes. A newer view refuses an older
@@ -4,3 +4,4 @@
4
4
  * Exports the RFQ (block trading) API implementation.
5
5
  */
6
6
  export { RfqAPIImpl } from "./api";
7
+ export { type RfqViewSource, waitForRfqView } from "./view";
@@ -4,3 +4,4 @@
4
4
  * Exports the RFQ (block trading) API implementation.
5
5
  */
6
6
  export { RfqAPIImpl } from "./api";
7
+ export { waitForRfqView } from "./view";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The taker's RFQ view from the `rfq` WebSocket channel.
3
+ *
4
+ * The RFQ service pushes the taker its view (`RfqView`) every time the quotes
5
+ * change, so a taker waiting for the quotes it wants should follow those
6
+ * pushes instead of polling `getRfq`: every `getRfq` builds the whole view on
7
+ * the RFQ service. {@link waitForRfqView} does that, and reads the RFQ only
8
+ * when a push may have been missed.
9
+ */
10
+ import type { MonacoWebSocket, RfqAPI, RfqView, RfqViewMatch, WaitForRfqViewOptions } from "@0xmonaco/types";
11
+ /** What {@link waitForRfqView} reads from: the SDK itself (`sdk`) has both. */
12
+ export interface RfqViewSource {
13
+ rfq: Pick<RfqAPI, "getRfq">;
14
+ ws: Pick<MonacoWebSocket, "rfq" | "isConnected" | "getSubscriptionState" | "onSubscriptionChange">;
15
+ }
16
+ /**
17
+ * Wait for the taker's view of `rfqId` to satisfy `predicate`, from the
18
+ * `RfqView` pushes on the `rfq` channel (the one with the highest `sequence`
19
+ * wins; pushes can arrive out of order).
20
+ *
21
+ * `getRfq` is read only where a push may have been missed:
22
+ * - once at the start, or, while the `rfq` subscribe is unanswered, once
23
+ * when it is confirmed: pushes sent before the wait began (makers can
24
+ * quote before `createRfq` returns) are not replayed to it;
25
+ * - once more on every confirmed resubscribe (after a reconnect);
26
+ * - repeatedly (250 ms, doubling to 2 s) while the socket is not connected or
27
+ * the subscription is not confirmed, including when it drops mid-wait;
28
+ * - once when `timeoutMs` passes without a match.
29
+ * A read that a newer push overlapped is dropped (the pushes decide); the
30
+ * final read is retried, up to 3 times, until one is not overlapped.
31
+ *
32
+ * Resolves with the first view that matches. Rejects with an
33
+ * `InvalidStateError` when no view matched by the timeout, or when an
34
+ * `RfqOutcome` push says the RFQ ended; with the signal's reason when aborted;
35
+ * and with the error of the final read if that read fails.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const rfq = await sdk.rfq.createRfq({ tradingPairId, product: "PERPETUAL", quantity: "25" });
40
+ * const rfqId = rfq.terms!.rfqId;
41
+ * const { view } = await waitForRfqView(sdk, rfqId, (view) => view.ask?.availableQuantity === "25", { initialView: rfq.view });
42
+ * await sdk.rfq.acceptRfq(rfqId, { side: "BUY", viewVersion: view.viewVersion });
43
+ * ```
44
+ */
45
+ export declare function waitForRfqView(source: RfqViewSource, rfqId: string, predicate: (view: RfqView) => boolean, options?: WaitForRfqViewOptions): Promise<RfqViewMatch>;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * The taker's RFQ view from the `rfq` WebSocket channel.
3
+ *
4
+ * The RFQ service pushes the taker its view (`RfqView`) every time the quotes
5
+ * change, so a taker waiting for the quotes it wants should follow those
6
+ * pushes instead of polling `getRfq`: every `getRfq` builds the whole view on
7
+ * the RFQ service. {@link waitForRfqView} does that, and reads the RFQ only
8
+ * when a push may have been missed.
9
+ */
10
+ import { UUIDSchema, validate } from "@0xmonaco/types";
11
+ import { InvalidStateError } from "../../errors";
12
+ const RFQ_CHANNEL = "rfq";
13
+ const DEFAULT_TIMEOUT_MS = 10_000;
14
+ /** Reads while no push can arrive start at this pause and double up to the cap. */
15
+ const FIRST_POLL_PAUSE_MS = 250;
16
+ const MAX_POLL_PAUSE_MS = 2_000;
17
+ /** Final reads tried when a newer push keeps overlapping them. */
18
+ const FINAL_READ_ATTEMPTS = 3;
19
+ /**
20
+ * Wait for the taker's view of `rfqId` to satisfy `predicate`, from the
21
+ * `RfqView` pushes on the `rfq` channel (the one with the highest `sequence`
22
+ * wins; pushes can arrive out of order).
23
+ *
24
+ * `getRfq` is read only where a push may have been missed:
25
+ * - once at the start, or, while the `rfq` subscribe is unanswered, once
26
+ * when it is confirmed: pushes sent before the wait began (makers can
27
+ * quote before `createRfq` returns) are not replayed to it;
28
+ * - once more on every confirmed resubscribe (after a reconnect);
29
+ * - repeatedly (250 ms, doubling to 2 s) while the socket is not connected or
30
+ * the subscription is not confirmed, including when it drops mid-wait;
31
+ * - once when `timeoutMs` passes without a match.
32
+ * A read that a newer push overlapped is dropped (the pushes decide); the
33
+ * final read is retried, up to 3 times, until one is not overlapped.
34
+ *
35
+ * Resolves with the first view that matches. Rejects with an
36
+ * `InvalidStateError` when no view matched by the timeout, or when an
37
+ * `RfqOutcome` push says the RFQ ended; with the signal's reason when aborted;
38
+ * and with the error of the final read if that read fails.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const rfq = await sdk.rfq.createRfq({ tradingPairId, product: "PERPETUAL", quantity: "25" });
43
+ * const rfqId = rfq.terms!.rfqId;
44
+ * const { view } = await waitForRfqView(sdk, rfqId, (view) => view.ask?.availableQuantity === "25", { initialView: rfq.view });
45
+ * await sdk.rfq.acceptRfq(rfqId, { side: "BUY", viewVersion: view.viewVersion });
46
+ * ```
47
+ */
48
+ export function waitForRfqView(source, rfqId, predicate, options = {}) {
49
+ const id = validate(UUIDSchema, rfqId);
50
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, initialView, signal } = options;
51
+ return new Promise((resolve, reject) => {
52
+ if (signal?.aborted) {
53
+ reject(signal.reason);
54
+ return;
55
+ }
56
+ let settled = false;
57
+ let latestSequence = -1;
58
+ /** `RfqView` pushes of this RFQ newer than every earlier one, so far. */
59
+ let pushesSeen = 0;
60
+ let pollTimer;
61
+ let pollPause = FIRST_POLL_PAUSE_MS;
62
+ let polling = false;
63
+ let reading = false;
64
+ let readAgain = false;
65
+ const cleanups = [];
66
+ const stop = () => {
67
+ settled = true;
68
+ polling = false;
69
+ clearTimeout(pollTimer);
70
+ for (const cleanup of cleanups.splice(0))
71
+ cleanup();
72
+ };
73
+ const finish = (match) => {
74
+ if (settled)
75
+ return;
76
+ stop();
77
+ resolve(match);
78
+ };
79
+ const fail = (error) => {
80
+ if (settled)
81
+ return;
82
+ stop();
83
+ reject(error);
84
+ };
85
+ /** True when `view` matched (and the wait is over). */
86
+ const consider = (view, from) => {
87
+ try {
88
+ if (predicate(view)) {
89
+ finish({ view, source: from });
90
+ return true;
91
+ }
92
+ }
93
+ catch (error) {
94
+ fail(error);
95
+ return true;
96
+ }
97
+ return false;
98
+ };
99
+ if (initialView && consider(initialView, "initial"))
100
+ return;
101
+ /**
102
+ * One `getRfq`; errors only matter to the final read. `overlapped` when a
103
+ * newer push arrived while it was in flight: a read has no `sequence`, so
104
+ * it may be older than that push. Anything newer than the latest push is
105
+ * pushed too (the service always pushes the last change), so while the
106
+ * wait goes on the pushes decide.
107
+ */
108
+ const read = async () => {
109
+ const before = pushesSeen;
110
+ const status = await source.rfq.getRfq(id);
111
+ return { view: status.view ?? undefined, overlapped: pushesSeen !== before };
112
+ };
113
+ const catchUp = async () => {
114
+ if (settled)
115
+ return;
116
+ if (reading) {
117
+ // The read in flight may predate what asked for this one.
118
+ readAgain = true;
119
+ return;
120
+ }
121
+ reading = true;
122
+ try {
123
+ const { view, overlapped } = await read();
124
+ if (!settled && view && !overlapped)
125
+ consider(view, "read");
126
+ }
127
+ catch {
128
+ // A push or the final read decides.
129
+ }
130
+ finally {
131
+ reading = false;
132
+ }
133
+ if (readAgain) {
134
+ readAgain = false;
135
+ await catchUp();
136
+ }
137
+ };
138
+ const stopPolling = () => {
139
+ polling = false;
140
+ clearTimeout(pollTimer);
141
+ pollTimer = undefined;
142
+ pollPause = FIRST_POLL_PAUSE_MS;
143
+ };
144
+ const poll = async () => {
145
+ pollTimer = undefined;
146
+ if (!polling || settled)
147
+ return;
148
+ await catchUp();
149
+ if (!polling || settled)
150
+ return;
151
+ if (pushesFlow()) {
152
+ stopPolling();
153
+ return;
154
+ }
155
+ pollTimer = setTimeout(poll, pollPause);
156
+ pollPause = Math.min(pollPause * 2, MAX_POLL_PAUSE_MS);
157
+ };
158
+ const startPolling = () => {
159
+ if (polling || settled)
160
+ return;
161
+ polling = true;
162
+ void poll();
163
+ };
164
+ const pushesFlow = () => source.ws.isConnected() && source.ws.getSubscriptionState(RFQ_CHANNEL) === "subscribed";
165
+ /** No push can arrive: the socket is down, or the subscribe was not taken. */
166
+ const pushesCannotArrive = () => {
167
+ const state = source.ws.getSubscriptionState(RFQ_CHANNEL);
168
+ return !source.ws.isConnected() || (state !== "subscribed" && state !== "pending");
169
+ };
170
+ // Subscribed first, so nothing pushed from here on is missed.
171
+ cleanups.push(source.ws.rfq((event) => {
172
+ if (settled || event.rfqId !== id)
173
+ return;
174
+ if (event.eventType === "RfqOutcome") {
175
+ fail(new InvalidStateError(`RFQ ${id} ended ${event.state} before a view matched`, event.state, "COLLECTING"));
176
+ return;
177
+ }
178
+ if (event.eventType !== "RfqView" || event.view.sequence <= latestSequence)
179
+ return;
180
+ latestSequence = event.view.sequence;
181
+ pushesSeen += 1;
182
+ consider(event.view, "push");
183
+ }));
184
+ // A confirmed (re)subscribe: what changed before it was never pushed to
185
+ // this handler, so read once and stop any polling.
186
+ cleanups.push(source.ws.onSubscriptionChange((change) => {
187
+ if (settled || change.channel !== RFQ_CHANNEL)
188
+ return;
189
+ if (change.state === "subscribed") {
190
+ stopPolling();
191
+ void catchUp();
192
+ }
193
+ else {
194
+ startPolling();
195
+ }
196
+ }));
197
+ // The last word: a read no newer push overlapped, tried a few times.
198
+ const timer = setTimeout(async () => {
199
+ if (settled)
200
+ return;
201
+ stopPolling();
202
+ try {
203
+ for (let attempt = 0; attempt < FINAL_READ_ATTEMPTS && !settled; attempt++) {
204
+ const { view, overlapped } = await read();
205
+ if (settled)
206
+ return;
207
+ if (overlapped)
208
+ continue;
209
+ if (view && consider(view, "read"))
210
+ return;
211
+ break;
212
+ }
213
+ fail(new InvalidStateError(`No view of RFQ ${id} matched within ${timeoutMs} ms`));
214
+ }
215
+ catch (error) {
216
+ fail(error);
217
+ }
218
+ }, timeoutMs);
219
+ cleanups.push(() => clearTimeout(timer));
220
+ if (signal) {
221
+ const onAbort = () => fail(signal.reason);
222
+ signal.addEventListener("abort", onAbort, { once: true });
223
+ cleanups.push(() => signal.removeEventListener("abort", onAbort));
224
+ }
225
+ // A dropped socket fires no subscription change: check it locally, and
226
+ // read until pushes can arrive again.
227
+ const liveness = setInterval(() => {
228
+ if (!settled && pushesCannotArrive())
229
+ startPolling();
230
+ }, FIRST_POLL_PAUSE_MS);
231
+ cleanups.push(() => clearInterval(liveness));
232
+ // What was pushed before this wait subscribed is gone: read it once now,
233
+ // or, while the subscribe is unanswered, when it is confirmed. With no
234
+ // push able to arrive yet, read until one can.
235
+ if (pushesCannotArrive()) {
236
+ startPolling();
237
+ }
238
+ else if (source.ws.getSubscriptionState(RFQ_CHANNEL) === "subscribed") {
239
+ void catchUp();
240
+ }
241
+ });
242
+ }
@@ -34,7 +34,7 @@ export declare function parseRawTradeEvent(raw: RawTradeEvent): TradeEvent;
34
34
  /**
35
35
  * Options for fetching trades
36
36
  */
37
- export interface GetTradesOptions {
37
+ interface TradesPageOptions {
38
38
  /** Maximum number of records to return (default: 25, max: 1000) */
39
39
  pageSize?: number;
40
40
  /**
@@ -43,10 +43,55 @@ export interface GetTradesOptions {
43
43
  */
44
44
  pageToken?: string;
45
45
  }
46
+ /**
47
+ * Options for fetching a pair's trades
48
+ */
49
+ export interface GetTradesOptions extends TradesPageOptions {
50
+ /**
51
+ * The execution source to keep: `"RFQ"` (RFQ block fills, the history
52
+ * behind `rfqTrades`) or `"CLOB"` (order-book fills), the values of each
53
+ * trade's `data.executionSource`. Omit for every fill. A page token is tied
54
+ * to the filter it was minted under.
55
+ */
56
+ executionSource?: "RFQ" | "CLOB";
57
+ }
58
+ /**
59
+ * Options for recovering missed RFQ prints.
60
+ */
61
+ export interface RecoverRfqTradesOptions {
62
+ /**
63
+ * The newest `executionId` the caller already has. Recovery stops at it;
64
+ * omit to read only the newest page.
65
+ */
66
+ sinceExecutionId?: string;
67
+ /** Rows per page (default: 100, max: 1000). */
68
+ pageSize?: number;
69
+ /**
70
+ * Most pages to read before giving up (default: 10). A `sinceExecutionId`
71
+ * not found within this many pages is reported as `complete: false`.
72
+ */
73
+ maxPages?: number;
74
+ }
75
+ /**
76
+ * The RFQ prints a caller missed, oldest first.
77
+ */
78
+ export interface RecoveredRfqTrades {
79
+ /**
80
+ * RFQ fills newer than `sinceExecutionId`, plus the other fills sharing its
81
+ * execution time, oldest first. Deduplicate by `executionId` on replay.
82
+ */
83
+ trades: TradeEvent[];
84
+ /**
85
+ * `true` when the walk reached `sinceExecutionId` or the start of the
86
+ * pair's history; `false` when it stopped at `maxPages`, so older prints may
87
+ * still be missing.
88
+ */
89
+ complete: boolean;
90
+ }
46
91
  /**
47
92
  * Options for the every-market trade feed.
48
93
  */
49
- export interface ListTradesOptions extends GetTradesOptions {
94
+ export interface ListTradesOptions extends TradesPageOptions {
50
95
  /** Only the trades this application (frontend) took, as the taker's application. */
51
96
  takerApplicationId?: string;
52
97
  }
@@ -90,6 +135,25 @@ export declare class TradesAPIImpl extends BaseAPI {
90
135
  * ```
91
136
  */
92
137
  getTrades(tradingPairId: string, options?: GetTradesOptions): Promise<GetTradesPage>;
138
+ /**
139
+ * Recover the RFQ block prints a `rfqTrades` subscriber missed (RFQ-15).
140
+ *
141
+ * `rfq_trades` prints are best-effort, so a gap is possible. This pages the
142
+ * pair's public RFQ history (`getTrades` with `executionSource: "RFQ"`),
143
+ * newest first, until it reaches `sinceExecutionId`, and returns what is
144
+ * newer than it, oldest first, ready to replay. Each trade's `tradeId` is
145
+ * the print's `executionId`.
146
+ *
147
+ * It also returns every other fill with the same execution time as
148
+ * `sinceExecutionId`: an RFQ's fills share one execution time and need not
149
+ * arrive in id order, so a missed one can sort after the last print seen.
150
+ * Some of those may already have been received, so deduplicate replayed
151
+ * fills by `executionId`.
152
+ *
153
+ * @param tradingPairId - The trading pair UUID
154
+ * @param options - Where to stop, and how far to look
155
+ */
156
+ recoverRfqTrades(tradingPairId: string, options?: RecoverRfqTradesOptions): Promise<RecoveredRfqTrades>;
93
157
  /**
94
158
  * Get one page of the trade feed: every market, newest first, recent and
95
159
  * archived trades alike, or only the trades one application took when
@@ -63,6 +63,9 @@ export class TradesAPIImpl extends BaseAPI {
63
63
  if (options.pageToken != null && options.pageToken !== "") {
64
64
  params.set("pageToken", options.pageToken);
65
65
  }
66
+ if (options.executionSource != null) {
67
+ params.set("executionSource", options.executionSource);
68
+ }
66
69
  const query = params.toString();
67
70
  const response = await this.makePublicRequest(`/api/v1/trades/${encodeURIComponent(tradingPairId)}${query === "" ? "" : `?${query}`}`);
68
71
  return {
@@ -75,6 +78,61 @@ export class TradesAPIImpl extends BaseAPI {
75
78
  nextPageToken: response.nextPageToken ?? "",
76
79
  };
77
80
  }
81
+ /**
82
+ * Recover the RFQ block prints a `rfqTrades` subscriber missed (RFQ-15).
83
+ *
84
+ * `rfq_trades` prints are best-effort, so a gap is possible. This pages the
85
+ * pair's public RFQ history (`getTrades` with `executionSource: "RFQ"`),
86
+ * newest first, until it reaches `sinceExecutionId`, and returns what is
87
+ * newer than it, oldest first, ready to replay. Each trade's `tradeId` is
88
+ * the print's `executionId`.
89
+ *
90
+ * It also returns every other fill with the same execution time as
91
+ * `sinceExecutionId`: an RFQ's fills share one execution time and need not
92
+ * arrive in id order, so a missed one can sort after the last print seen.
93
+ * Some of those may already have been received, so deduplicate replayed
94
+ * fills by `executionId`.
95
+ *
96
+ * @param tradingPairId - The trading pair UUID
97
+ * @param options - Where to stop, and how far to look
98
+ */
99
+ async recoverRfqTrades(tradingPairId, options = {}) {
100
+ const pageSize = options.pageSize ?? 100;
101
+ const maxPages = options.maxPages ?? 10;
102
+ if (!Number.isInteger(maxPages) || maxPages < 1) {
103
+ throw new Error("maxPages must be a positive integer");
104
+ }
105
+ const newestFirst = [];
106
+ // Set once the walk reaches `sinceExecutionId`: that print's execution
107
+ // time. RFQ children share one execution time and their ids need not
108
+ // follow receipt order, so a missed child can sort after the last seen
109
+ // one; the walk reads on through the whole group before it stops.
110
+ let anchorExecutedAt;
111
+ let pageToken;
112
+ for (let pages = 0; pages < maxPages; pages++) {
113
+ const page = await this.getTrades(tradingPairId, { pageSize, executionSource: "RFQ", ...(pageToken ? { pageToken } : {}) });
114
+ for (const trade of page.trades) {
115
+ if (anchorExecutedAt !== undefined) {
116
+ if (trade.data.executedAt !== anchorExecutedAt) {
117
+ return { trades: newestFirst.reverse(), complete: true };
118
+ }
119
+ newestFirst.push(trade);
120
+ continue;
121
+ }
122
+ if (options.sinceExecutionId != null && trade.data.tradeId === options.sinceExecutionId) {
123
+ anchorExecutedAt = trade.data.executedAt;
124
+ continue;
125
+ }
126
+ newestFirst.push(trade);
127
+ }
128
+ // Without a known last print, only the newest page is wanted.
129
+ if (options.sinceExecutionId == null || page.nextPageToken === "") {
130
+ return { trades: newestFirst.reverse(), complete: true };
131
+ }
132
+ pageToken = page.nextPageToken;
133
+ }
134
+ return { trades: newestFirst.reverse(), complete: false };
135
+ }
78
136
  /**
79
137
  * Get one page of the trade feed: every market, newest first, recent and
80
138
  * archived trades alike, or only the trades one application took when
@@ -323,11 +323,10 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
323
323
  * Failed replacements do not prevent other orders from being replaced.
324
324
  *
325
325
  * Pre-sequencing validation is scoped per item too, matching `batchCreate`:
326
- * a malformed `orderId`, `price`, `quantity` or `clientOrderId`, or a
327
- * `postOnly` violation, fails only that item — its result slot carries a
328
- * structured `error.code` (`INVALID_ORDER_ID`, `INVALID_PRICE`,
329
- * `INVALID_QUANTITY`, `INVALID_CLIENT_ORDER_ID`, `INVALID_POST_ONLY`) and its
330
- * original is left untouched — while the valid items proceed. A `2xx`
326
+ * a malformed `orderId`, `price`, `quantity` or `clientOrderId` fails only
327
+ * that item — its result slot carries a structured `error.code`
328
+ * (`INVALID_ORDER_ID`, `INVALID_PRICE`, `INVALID_QUANTITY`,
329
+ * `INVALID_CLIENT_ORDER_ID`) and its original is left untouched — while the valid items proceed. A `2xx`
331
330
  * response can therefore mix executed replacements and failed items: read
332
331
  * `results[]` per item rather than treating the status code as the verdict.
333
332
  * Only an empty or oversize (>100) batch rejects the whole request.
@@ -495,11 +495,10 @@ export class TradingAPIImpl extends BaseAPI {
495
495
  * Failed replacements do not prevent other orders from being replaced.
496
496
  *
497
497
  * Pre-sequencing validation is scoped per item too, matching `batchCreate`:
498
- * a malformed `orderId`, `price`, `quantity` or `clientOrderId`, or a
499
- * `postOnly` violation, fails only that item — its result slot carries a
500
- * structured `error.code` (`INVALID_ORDER_ID`, `INVALID_PRICE`,
501
- * `INVALID_QUANTITY`, `INVALID_CLIENT_ORDER_ID`, `INVALID_POST_ONLY`) and its
502
- * original is left untouched — while the valid items proceed. A `2xx`
498
+ * a malformed `orderId`, `price`, `quantity` or `clientOrderId` fails only
499
+ * that item — its result slot carries a structured `error.code`
500
+ * (`INVALID_ORDER_ID`, `INVALID_PRICE`, `INVALID_QUANTITY`,
501
+ * `INVALID_CLIENT_ORDER_ID`) and its original is left untouched — while the valid items proceed. A `2xx`
503
502
  * response can therefore mix executed replacements and failed items: read
504
503
  * `results[]` per item rather than treating the status code as the verdict.
505
504
  * Only an empty or oversize (>100) batch rejects the whole request.
@@ -728,6 +728,7 @@ function parseMarketStatsSummary(raw) {
728
728
  fundingRate: readOptionalString(data, "fundingRate"),
729
729
  premium: readOptionalString(data, "premium"),
730
730
  openInterestNotional: readOptionalString(data, "openInterestNotional"),
731
+ dailyVolumeBySource: parseDailyVolumeBySource(data.dailyVolumeBySource),
731
732
  };
732
733
  }
733
734
  function parseMarketStatsAllEvent(rawData) {
@@ -3075,7 +3076,7 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
3075
3076
  }
3076
3077
  });
3077
3078
  };
3078
- const subscribeRfqTrades = (tradingPairId, handler) => {
3079
+ const subscribeRfqTrades = (tradingPairId, handler, onSnapshot) => {
3079
3080
  if (!handler) {
3080
3081
  throw new Error("rfqTrades subscription requires a handler");
3081
3082
  }
@@ -3086,7 +3087,9 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
3086
3087
  catch (err) {
3087
3088
  console.error("WebSocket: Error processing RFQ trade event", err);
3088
3089
  }
3089
- });
3090
+ },
3091
+ // The pair's newest RFQ prints from the public RFQ history (RFQ-15).
3092
+ onSnapshot && snapshotFrameHandler("rfq_trades", parseRfqTradeEventData, onSnapshot));
3090
3093
  };
3091
3094
  return {
3092
3095
  connect,
@@ -30,6 +30,7 @@ export declare const COVERED: {
30
30
  get_rfq_capabilities: string;
31
31
  create_rfq: string;
32
32
  get_rfq: string;
33
+ list_rfqs: string;
33
34
  accept_rfq: string;
34
35
  cancel_rfq: string;
35
36
  upsert_rfq_quote: string;
@@ -138,6 +139,7 @@ export declare const COVERED: {
138
139
  list_pending_withdrawals: string;
139
140
  list_all_funding_history: string;
140
141
  list_application_balances: string;
142
+ list_application_margin_accounts: string;
141
143
  list_application_movements: string;
142
144
  list_application_orders: string;
143
145
  list_application_users: string;
package/dist/coverage.js CHANGED
@@ -30,6 +30,7 @@ export const COVERED = {
30
30
  get_rfq_capabilities: "rfq.getCapabilities",
31
31
  create_rfq: "rfq.createRfq",
32
32
  get_rfq: "rfq.getRfq",
33
+ list_rfqs: "rfq.listRfqs",
33
34
  accept_rfq: "rfq.acceptRfq",
34
35
  cancel_rfq: "rfq.cancelRfq",
35
36
  upsert_rfq_quote: "rfq.upsertQuote",
@@ -138,6 +139,7 @@ export const COVERED = {
138
139
  list_pending_withdrawals: "listPendingWithdrawals",
139
140
  list_all_funding_history: "listAllFundingHistory",
140
141
  list_application_balances: "listApplicationBalances",
142
+ list_application_margin_accounts: "listApplicationMarginAccounts",
141
143
  list_application_movements: "listApplicationMovements",
142
144
  list_application_orders: "listApplicationOrders",
143
145
  list_application_users: "listApplicationUsers",
@@ -673,6 +673,14 @@ function getErrorSuggestion(statusCode, message, responseBody) {
673
673
  retryable: false,
674
674
  };
675
675
  }
676
+ // A transfer stopped partway: its first moves stand, and sending it again would
677
+ // move the money again. Check BEFORE the generic 409.
678
+ if (responseBody?.code === "COLLATERAL_TRANSFER_STOPPED_PARTWAY") {
679
+ return {
680
+ suggestion: "The transfer stopped partway and the amount is where this message says. Do not retry it: move the amount from there.",
681
+ retryable: false,
682
+ };
683
+ }
676
684
  // Conflict (409) - check BEFORE 500+ to avoid being shadowed
677
685
  if (statusCode === StatusCodes.CONFLICT) {
678
686
  return {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { type RfqViewSource, waitForRfqView } from "./api/rfq";
1
2
  export { createMonacoWebSocket, type MonacoWebSocket, type MonacoWebSocketOptions } from "./api/websocket";
2
3
  export { APIError, ContractError, InvalidConfigError, InvalidStateError, MonacoCoreError, WithdrawalRateLimitedError } from "./errors";
3
4
  export { resolveApiUrl, resolveWsUrl } from "./networks";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { waitForRfqView } from "./api/rfq";
1
2
  export { createMonacoWebSocket } from "./api/websocket";
2
3
  export { APIError, ContractError, InvalidConfigError, InvalidStateError, MonacoCoreError, WithdrawalRateLimitedError } from "./errors";
3
4
  export { resolveApiUrl, resolveWsUrl } from "./networks";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "type": "module",
5
5
  "homepage": "https://docs.0xmonaco.com/sdk/typescript",
6
6
  "main": "./dist/index.js",
@@ -19,15 +19,15 @@
19
19
  "viem": "^2.45.2"
20
20
  },
21
21
  "dependencies": {
22
- "@0xmonaco/contracts": "1.1.8",
23
- "@0xmonaco/types": "1.1.8",
22
+ "@0xmonaco/contracts": "1.1.10",
23
+ "@0xmonaco/types": "1.1.10",
24
24
  "@noble/curves": "^2.4.0",
25
25
  "@noble/hashes": "^2.4.0",
26
26
  "http-status-codes": "^2.3.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "dotenv": "^18.0.8",
30
- "ws": "^8.22.0"
30
+ "ws": "^8.22.1"
31
31
  },
32
32
  "exports": {
33
33
  ".": {