@0xmonaco/core 1.0.29 → 1.0.32-develop.522174b

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/dist/api/base.js CHANGED
@@ -34,6 +34,112 @@ import { APIError } from "../errors";
34
34
  * only takes effect in server runtimes (Node/undici, Bun, Deno).
35
35
  */
36
36
  const SDK_USER_AGENT = "@0xmonaco/core";
37
+ const MONTHS = {
38
+ Jan: 0,
39
+ Feb: 1,
40
+ Mar: 2,
41
+ Apr: 3,
42
+ May: 4,
43
+ Jun: 5,
44
+ Jul: 6,
45
+ Aug: 7,
46
+ Sep: 8,
47
+ Oct: 9,
48
+ Nov: 10,
49
+ Dec: 11,
50
+ };
51
+ // The three RFC 7231 §7.1.1.1 `HTTP-date` grammars a recipient must accept.
52
+ // Each is anchored so trailing junk is rejected, and `Date.parse` is avoided
53
+ // entirely (it is permissive — it reads asctime in local time and rolls
54
+ // impossible dates like 31 Feb forward instead of rejecting them).
55
+ const MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
56
+ const IMF_FIXDATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (${MONTH}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
57
+ const RFC850_DATE = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d{2})-(${MONTH})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
58
+ const ASCTIME_DATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (${MONTH}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
59
+ /**
60
+ * Expand an RFC 850 two-digit year per RFC 7231 §7.1.1.1: a full timestamp that
61
+ * appears more than 50 years in the future is interpreted as the most recent
62
+ * past year with the same last two digits. The comparison is against the exact
63
+ * receipt instant plus 50 years (not just calendar years), so the boundary can
64
+ * fall mid-year — e.g. received 2026-06-01, `31-Dec-76` is 50y7m ahead and maps
65
+ * to 1976, while `01-Jan-76` is under 50 years ahead and stays 2076.
66
+ */
67
+ function expandTwoDigitYear(twoDigitYear, month, day, hour, minute, second) {
68
+ const now = new Date(Date.now());
69
+ const candidateYear = Math.floor(now.getUTCFullYear() / 100) * 100 + twoDigitYear;
70
+ const candidate = Date.UTC(candidateYear, month, day, hour, minute, second);
71
+ const fiftyYearsAhead = Date.UTC(now.getUTCFullYear() + 50, now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
72
+ return candidate > fiftyYearsAhead ? candidateYear - 100 : candidateYear;
73
+ }
74
+ /**
75
+ * Parse an RFC 7231 `HTTP-date` — in any of the three formats a recipient must
76
+ * accept (IMF-fixdate, the obsolete RFC 850 form, and asctime) — into a UTC
77
+ * epoch in milliseconds, or `undefined` if the value is not a well-formed, real
78
+ * calendar date. All three denote UTC, so timestamps are built with `Date.UTC`
79
+ * and the fields are round-tripped to reject impossible dates such as
80
+ * `31 Feb` or `25:00:00`. RFC 850's 2-digit year follows the RFC 7231 rule
81
+ * (see {@link expandTwoDigitYear}). The redundant day-name is intentionally not
82
+ * validated against the date — RFC 7231 encourages robust parsing, and the
83
+ * date/time fields already determine the instant unambiguously, so a mismatched
84
+ * weekday still yields the intended delay rather than a dropped header and an
85
+ * arbitrary fallback.
86
+ */
87
+ function parseHttpDate(value) {
88
+ let day;
89
+ let month;
90
+ let year;
91
+ let hour;
92
+ let minute;
93
+ let second;
94
+ const imf = IMF_FIXDATE.exec(value);
95
+ const rfc850 = RFC850_DATE.exec(value);
96
+ const asctime = ASCTIME_DATE.exec(value);
97
+ if (imf) {
98
+ day = Number(imf[1]);
99
+ month = MONTHS[imf[2]];
100
+ year = Number(imf[3]);
101
+ hour = Number(imf[4]);
102
+ minute = Number(imf[5]);
103
+ second = Number(imf[6]);
104
+ }
105
+ else if (rfc850) {
106
+ day = Number(rfc850[1]);
107
+ month = MONTHS[rfc850[2]];
108
+ hour = Number(rfc850[4]);
109
+ minute = Number(rfc850[5]);
110
+ second = Number(rfc850[6]);
111
+ if (month === undefined)
112
+ return undefined;
113
+ year = expandTwoDigitYear(Number(rfc850[3]), month, day, hour, minute, second);
114
+ }
115
+ else if (asctime) {
116
+ month = MONTHS[asctime[1]];
117
+ day = Number((asctime[2] ?? "").trim());
118
+ hour = Number(asctime[3]);
119
+ minute = Number(asctime[4]);
120
+ second = Number(asctime[5]);
121
+ year = Number(asctime[6]);
122
+ }
123
+ else {
124
+ return undefined;
125
+ }
126
+ if (month === undefined)
127
+ return undefined;
128
+ const timestamp = Date.UTC(year, month, day, hour, minute, second);
129
+ const utc = new Date(timestamp);
130
+ // Reject impossible calendar/clock values that Date.UTC silently rolls over
131
+ // (e.g. 31 Feb, 25:00:00). A `:60` leap second is likewise dropped rather
132
+ // than special-cased — leap seconds never appear in a Retry-After header.
133
+ if (utc.getUTCFullYear() !== year ||
134
+ utc.getUTCMonth() !== month ||
135
+ utc.getUTCDate() !== day ||
136
+ utc.getUTCHours() !== hour ||
137
+ utc.getUTCMinutes() !== minute ||
138
+ utc.getUTCSeconds() !== second) {
139
+ return undefined;
140
+ }
141
+ return timestamp;
142
+ }
37
143
  export class BaseAPI {
38
144
  apiUrl;
39
145
  /** Active session keypair (raw bytes) used to sign authenticated requests. */
@@ -138,12 +244,25 @@ export class BaseAPI {
138
244
  if (!headers)
139
245
  return {};
140
246
  const requestId = headers.get("x-request-id") || headers.get("request-id") || headers.get("x-correlation-id") || undefined;
141
- // Parse Retry-After header with validation
247
+ // Parse the Retry-After header (RFC 7231). It is either a non-negative
248
+ // number of seconds (delay-seconds) or an HTTP-date. Both forms are
249
+ // normalized to a delay in SECONDS, so callers multiply by 1000 for
250
+ // setTimeout. An HTTP-date is converted to the remaining seconds from now
251
+ // (past dates clamp to 0). Values matching neither grammar are dropped so
252
+ // a malformed header can never masquerade as a 0s "retry immediately".
142
253
  const retryAfterHeader = headers.get("retry-after");
143
254
  let retryAfter;
144
255
  if (retryAfterHeader !== null) {
145
- const parsed = Number.parseInt(retryAfterHeader, 10);
146
- retryAfter = Number.isNaN(parsed) ? undefined : parsed;
256
+ const value = retryAfterHeader.trim();
257
+ if (/^\d+$/.test(value)) {
258
+ retryAfter = Number.parseInt(value, 10);
259
+ }
260
+ else {
261
+ const dateMs = parseHttpDate(value);
262
+ if (dateMs !== undefined) {
263
+ retryAfter = Math.max(0, Math.ceil((dateMs - Date.now()) / 1000));
264
+ }
265
+ }
147
266
  }
148
267
  return { requestId, retryAfter };
149
268
  }
@@ -169,30 +169,65 @@ export declare class ProfileAPIImpl extends BaseAPI implements ProfileAPI {
169
169
  * Fetches the user's executed trades from the /api/v1/accounts/trades endpoint.
170
170
  * Requires a valid access token to be set.
171
171
  *
172
+ * When neither `page` nor `pageToken` is passed, the SDK drives cursor (pageToken)
173
+ * pagination: it starts a cursor walk from the newest row and the response's
174
+ * `nextPageToken` resumes it. Cursor responses carry no exact totals
175
+ * (`total`/`page`/`totalPages` are 0).
176
+ *
172
177
  * @param params - Optional query parameters for pagination and filtering
173
- * @param params.page - Page number (starts from 1, default: 1)
174
- * @param params.pageSize - Number of items per page (default: 20, max: 100)
178
+ * @param params.page - Deprecated: page number for legacy offset pagination (starts
179
+ * from 1). Passing it selects legacy page-number mode, which only sees recent (hot)
180
+ * history and populates `total`/`totalPages`. Use cursor pagination (`pageToken`)
181
+ * instead.
182
+ * @param params.pageSize - Number of items per page (default: 20; max 100
183
+ * in page-number mode, up to 1000 when pageToken is present)
175
184
  * @param params.tradingPairId - Filter by trading pair ID (UUID)
176
- * @param params.pageToken - Pagination cursor (optional): omit for legacy page-number
177
- * pagination; pass `""` to start a cursor walk from the newest row; pass a previous
178
- * response's `nextPageToken` to resume. Any cursor-walk value (`""` or non-empty)
179
- * ignores `page` and covers full trade history (hot and archived rows merged), unlike
180
- * page-number mode.
185
+ * @param params.pageToken - Pagination cursor: pass `""` to start a cursor walk from
186
+ * the newest row; pass a previous response's `nextPageToken` to resume. Any
187
+ * cursor-walk value (`""` or non-empty) ignores `page` and covers full trade history
188
+ * (hot and archived rows merged), unlike page-number mode. Omitting both `page` and
189
+ * `pageToken` starts a cursor walk (`pageToken=""`).
181
190
  * @returns Promise resolving to paginated trades response
182
191
  * @throws {ValidationError} When the provided params fail client-side validation
183
192
  * @throws {APIError} When the request fails or user is not authenticated
184
193
  *
185
194
  * @example
186
195
  * ```typescript
187
- * // Get first page of trades
188
- * const trades = await profileAPI.getUserTrades();
189
- * console.log(`Total trades: ${trades.total}`);
196
+ * // Walk full trade history with cursor pagination (the default mode)
197
+ * let pageToken: string | undefined;
198
+ * do {
199
+ * const res = await profileAPI.getUserTrades({ pageToken });
200
+ * handle(res.trades);
201
+ * pageToken = res.nextPageToken;
202
+ * } while (pageToken);
190
203
  *
191
204
  * // Filter by trading pair
192
205
  * const pairTrades = await profileAPI.getUserTrades({ tradingPairId: "550e8400-e29b-41d4-a716-446655440000" });
193
206
  * ```
194
207
  */
195
208
  getUserTrades(params?: GetUserTradesParams): Promise<GetUserTradesResponse>;
209
+ /**
210
+ * Walks the user's full trade history with cursor pagination, yielding one
211
+ * trade at a time.
212
+ *
213
+ * Starts a cursor walk (`pageToken: ""`) and follows each response's
214
+ * `nextPageToken` until the server proves exhaustion (a page with zero rows
215
+ * and an empty token). Covers full trade history — hot and archived rows
216
+ * merged — unlike the deprecated page-number mode.
217
+ *
218
+ * @param params - Optional filters and page size; `page`/`pageToken` are
219
+ * managed by the iterator and cannot be passed.
220
+ * @returns Async generator yielding trades newest-first
221
+ * @throws {APIError} When any underlying request fails
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * for await (const trade of profileAPI.iterateUserTrades()) {
226
+ * console.log(trade.tradeId);
227
+ * }
228
+ * ```
229
+ */
230
+ iterateUserTrades(params?: Omit<GetUserTradesParams, "page" | "pageToken">): AsyncGenerator<GetUserTradesResponse["trades"][number], void, undefined>;
196
231
  /**
197
232
  * List the current user's funding payment history with pagination and filters.
198
233
  *
@@ -220,24 +220,37 @@ export class ProfileAPIImpl extends BaseAPI {
220
220
  * Fetches the user's executed trades from the /api/v1/accounts/trades endpoint.
221
221
  * Requires a valid access token to be set.
222
222
  *
223
+ * When neither `page` nor `pageToken` is passed, the SDK drives cursor (pageToken)
224
+ * pagination: it starts a cursor walk from the newest row and the response's
225
+ * `nextPageToken` resumes it. Cursor responses carry no exact totals
226
+ * (`total`/`page`/`totalPages` are 0).
227
+ *
223
228
  * @param params - Optional query parameters for pagination and filtering
224
- * @param params.page - Page number (starts from 1, default: 1)
225
- * @param params.pageSize - Number of items per page (default: 20, max: 100)
229
+ * @param params.page - Deprecated: page number for legacy offset pagination (starts
230
+ * from 1). Passing it selects legacy page-number mode, which only sees recent (hot)
231
+ * history and populates `total`/`totalPages`. Use cursor pagination (`pageToken`)
232
+ * instead.
233
+ * @param params.pageSize - Number of items per page (default: 20; max 100
234
+ * in page-number mode, up to 1000 when pageToken is present)
226
235
  * @param params.tradingPairId - Filter by trading pair ID (UUID)
227
- * @param params.pageToken - Pagination cursor (optional): omit for legacy page-number
228
- * pagination; pass `""` to start a cursor walk from the newest row; pass a previous
229
- * response's `nextPageToken` to resume. Any cursor-walk value (`""` or non-empty)
230
- * ignores `page` and covers full trade history (hot and archived rows merged), unlike
231
- * page-number mode.
236
+ * @param params.pageToken - Pagination cursor: pass `""` to start a cursor walk from
237
+ * the newest row; pass a previous response's `nextPageToken` to resume. Any
238
+ * cursor-walk value (`""` or non-empty) ignores `page` and covers full trade history
239
+ * (hot and archived rows merged), unlike page-number mode. Omitting both `page` and
240
+ * `pageToken` starts a cursor walk (`pageToken=""`).
232
241
  * @returns Promise resolving to paginated trades response
233
242
  * @throws {ValidationError} When the provided params fail client-side validation
234
243
  * @throws {APIError} When the request fails or user is not authenticated
235
244
  *
236
245
  * @example
237
246
  * ```typescript
238
- * // Get first page of trades
239
- * const trades = await profileAPI.getUserTrades();
240
- * console.log(`Total trades: ${trades.total}`);
247
+ * // Walk full trade history with cursor pagination (the default mode)
248
+ * let pageToken: string | undefined;
249
+ * do {
250
+ * const res = await profileAPI.getUserTrades({ pageToken });
251
+ * handle(res.trades);
252
+ * pageToken = res.nextPageToken;
253
+ * } while (pageToken);
241
254
  *
242
255
  * // Filter by trading pair
243
256
  * const pairTrades = await profileAPI.getUserTrades({ tradingPairId: "550e8400-e29b-41d4-a716-446655440000" });
@@ -247,8 +260,11 @@ export class ProfileAPIImpl extends BaseAPI {
247
260
  if (params) {
248
261
  validate(GetUserTradesSchema, params);
249
262
  }
263
+ // Legacy offset mode only when the caller explicitly asked for a page and
264
+ // no cursor was given; everything else drives cursor (pageToken) mode.
265
+ const legacyOffsetMode = params?.page !== undefined && params?.pageToken === undefined;
250
266
  const searchParams = new URLSearchParams();
251
- if (params?.page !== undefined) {
267
+ if (legacyOffsetMode && params?.page !== undefined) {
252
268
  searchParams.append("page", params.page.toString());
253
269
  }
254
270
  if (params?.pageSize !== undefined) {
@@ -257,13 +273,53 @@ export class ProfileAPIImpl extends BaseAPI {
257
273
  if (params?.tradingPairId !== undefined) {
258
274
  searchParams.append("tradingPairId", params.tradingPairId);
259
275
  }
260
- if (params?.pageToken !== undefined) {
261
- searchParams.append("pageToken", params.pageToken);
276
+ // Cursor mode selects itself by the *presence* of `pageToken` on the wire,
277
+ // even when it is the empty string (the documented way to start a walk),
278
+ // so it must be sent explicitly rather than dropped like a falsy value.
279
+ // When the caller gave neither `page` nor `pageToken`, default to starting
280
+ // a cursor walk (`pageToken=""`).
281
+ if (!legacyOffsetMode) {
282
+ searchParams.append("pageToken", params?.pageToken ?? "");
262
283
  }
263
284
  const queryString = searchParams.toString();
264
285
  const url = queryString ? `/api/v1/accounts/trades?${queryString}` : "/api/v1/accounts/trades";
265
286
  return await this.makeAuthenticatedRequest(url);
266
287
  }
288
+ /**
289
+ * Walks the user's full trade history with cursor pagination, yielding one
290
+ * trade at a time.
291
+ *
292
+ * Starts a cursor walk (`pageToken: ""`) and follows each response's
293
+ * `nextPageToken` until the server proves exhaustion (a page with zero rows
294
+ * and an empty token). Covers full trade history — hot and archived rows
295
+ * merged — unlike the deprecated page-number mode.
296
+ *
297
+ * @param params - Optional filters and page size; `page`/`pageToken` are
298
+ * managed by the iterator and cannot be passed.
299
+ * @returns Async generator yielding trades newest-first
300
+ * @throws {APIError} When any underlying request fails
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * for await (const trade of profileAPI.iterateUserTrades()) {
305
+ * console.log(trade.tradeId);
306
+ * }
307
+ * ```
308
+ */
309
+ async *iterateUserTrades(params) {
310
+ let pageToken = "";
311
+ // A non-empty token never guarantees further results (AIP-158 "maybe
312
+ // more"): keep following it until a response returns zero rows and an
313
+ // empty token, the sole proof the walk is exhausted.
314
+ for (;;) {
315
+ const response = await this.getUserTrades({ ...params, pageToken });
316
+ yield* response.trades;
317
+ if (!response.nextPageToken) {
318
+ return;
319
+ }
320
+ pageToken = response.nextPageToken;
321
+ }
322
+ }
267
323
  /**
268
324
  * List the current user's funding payment history with pagination and filters.
269
325
  *
@@ -260,40 +260,66 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
260
260
  */
261
261
  batchReplace(orders: BatchReplaceOrderParams[]): Promise<BatchReplaceOrdersResponse>;
262
262
  /**
263
- * Gets paginated orders based on query parameters with automatic pagination.
263
+ * Gets orders based on query parameters, cursor-paginated by default.
264
264
  *
265
265
  * Retrieves orders with optional filtering by status, trading pair, and pagination.
266
- * Pagination parameters are always included with sensible defaults for consistent API behavior.
266
+ * When neither `page` nor `pageToken` is passed, the SDK drives cursor (pageToken)
267
+ * pagination: it starts a cursor walk from the newest (or oldest, for
268
+ * `orderBy: "ASC"`) row and the response's `nextPageToken` resumes it. Cursor
269
+ * responses carry no exact totals (`total`/`page`/`totalPages` are 0).
267
270
  *
268
271
  * @param params - Query parameters for filtering orders
269
- * @param params.status - Filter by order status (e.g., "SUBMITTED", "FILLED") (optional)
272
+ * @param params.status - Filter by order status (e.g., "SUBMITTED", "FILLED"), or by
273
+ * any of several statuses at once by passing an array (e.g., ["SUBMITTED", "PARTIALLY_FILLED"]) (optional)
270
274
  * @param params.tradingPairId - Filter by trading pair UUID (optional)
271
- * @param params.page - Page number for pagination (defaults to 1, must be > 0)
272
- * @param params.pageSize - Number of orders per page (defaults to 10, max 100, must be > 0)
273
- * @param params.pageToken - Pagination cursor (optional): omit for legacy page-number
274
- * pagination; pass `""` to start a cursor walk from the newest (or oldest, for
275
- * `orderBy: "ASC"`) row; pass a previous response's `nextPageToken` to resume. Any
276
- * cursor-walk value (`""` or non-empty) ignores `page` and covers full order history
277
- * (hot and archived rows merged), unlike page-number mode.
275
+ * @param params.page - Deprecated: page number for legacy offset pagination (must be > 0).
276
+ * Passing it selects legacy page-number mode, which only sees recent (hot) history and
277
+ * populates `total`/`totalPages`. Use cursor pagination (`pageToken`) instead.
278
+ * @param params.pageSize - Number of orders per page (defaults to 10, must be > 0; max 100
279
+ * in page-number mode, up to 1000 when pageToken is present)
280
+ * @param params.pageToken - Pagination cursor: pass `""` to start a cursor walk from
281
+ * the newest (or oldest, for `orderBy: "ASC"`) row; pass a previous response's
282
+ * `nextPageToken` to resume. Any cursor-walk value (`""` or non-empty) ignores `page`
283
+ * and covers full order history (hot and archived rows merged), unlike page-number
284
+ * mode. Omitting both `page` and `pageToken` starts a cursor walk (`pageToken=""`).
278
285
  * @returns Promise resolving to GetPaginatedOrdersResponse with orders and pagination info
279
286
  * @throws {APIError} When API communication fails
280
287
  *
281
288
  * @example
282
289
  * ```typescript
283
- * // Get submitted orders for a specific trading pair with custom pagination
284
- * const orders = await tradingAPI.getPaginatedOrders({
285
- * status: "SUBMITTED",
286
- * tradingPairId: "456e7890-e12b-12d3-a456-426614174000",
287
- * page: 1,
288
- * pageSize: 20
289
- * });
290
- * console.log(`Found ${orders.total} orders, showing page ${orders.page}`);
291
- *
292
- * // Get all orders with default pagination (page=1, pageSize=10)
293
- * const allOrders = await tradingAPI.getPaginatedOrders();
290
+ * // Walk full order history with cursor pagination (the default mode)
291
+ * let pageToken: string | undefined;
292
+ * do {
293
+ * const res = await tradingAPI.getPaginatedOrders({ status: "FILLED", pageToken });
294
+ * handle(res.orders);
295
+ * pageToken = res.nextPageToken;
296
+ * } while (pageToken);
294
297
  * ```
295
298
  */
296
299
  getPaginatedOrders(params?: GetPaginatedOrdersParams): Promise<GetPaginatedOrdersResponse>;
300
+ /**
301
+ * Walks the user's full order history with cursor pagination, yielding one
302
+ * order at a time.
303
+ *
304
+ * Starts a cursor walk (`pageToken: ""`) and follows each response's
305
+ * `nextPageToken` until the server proves exhaustion (a page with zero rows
306
+ * and an empty token). Covers full order history — hot and archived rows
307
+ * merged — unlike the deprecated page-number mode.
308
+ *
309
+ * @param params - Optional filters and page size; `page`/`pageToken` are
310
+ * managed by the iterator and cannot be passed.
311
+ * @returns Async generator yielding orders newest-first (or oldest-first for
312
+ * `orderBy: "ASC"`)
313
+ * @throws {APIError} When any underlying request fails
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * for await (const order of tradingAPI.iterateOrders({ status: "FILLED" })) {
318
+ * console.log(order.id);
319
+ * }
320
+ * ```
321
+ */
322
+ iterateOrders(params?: Omit<GetPaginatedOrdersParams, "page" | "pageToken">): AsyncGenerator<GetPaginatedOrdersResponse["orders"][number], void, undefined>;
297
323
  /**
298
324
  * Gets a single order by its ID.
299
325
  *
@@ -409,37 +409,40 @@ export class TradingAPIImpl extends BaseAPI {
409
409
  });
410
410
  }
411
411
  /**
412
- * Gets paginated orders based on query parameters with automatic pagination.
412
+ * Gets orders based on query parameters, cursor-paginated by default.
413
413
  *
414
414
  * Retrieves orders with optional filtering by status, trading pair, and pagination.
415
- * Pagination parameters are always included with sensible defaults for consistent API behavior.
415
+ * When neither `page` nor `pageToken` is passed, the SDK drives cursor (pageToken)
416
+ * pagination: it starts a cursor walk from the newest (or oldest, for
417
+ * `orderBy: "ASC"`) row and the response's `nextPageToken` resumes it. Cursor
418
+ * responses carry no exact totals (`total`/`page`/`totalPages` are 0).
416
419
  *
417
420
  * @param params - Query parameters for filtering orders
418
- * @param params.status - Filter by order status (e.g., "SUBMITTED", "FILLED") (optional)
421
+ * @param params.status - Filter by order status (e.g., "SUBMITTED", "FILLED"), or by
422
+ * any of several statuses at once by passing an array (e.g., ["SUBMITTED", "PARTIALLY_FILLED"]) (optional)
419
423
  * @param params.tradingPairId - Filter by trading pair UUID (optional)
420
- * @param params.page - Page number for pagination (defaults to 1, must be > 0)
421
- * @param params.pageSize - Number of orders per page (defaults to 10, max 100, must be > 0)
422
- * @param params.pageToken - Pagination cursor (optional): omit for legacy page-number
423
- * pagination; pass `""` to start a cursor walk from the newest (or oldest, for
424
- * `orderBy: "ASC"`) row; pass a previous response's `nextPageToken` to resume. Any
425
- * cursor-walk value (`""` or non-empty) ignores `page` and covers full order history
426
- * (hot and archived rows merged), unlike page-number mode.
424
+ * @param params.page - Deprecated: page number for legacy offset pagination (must be > 0).
425
+ * Passing it selects legacy page-number mode, which only sees recent (hot) history and
426
+ * populates `total`/`totalPages`. Use cursor pagination (`pageToken`) instead.
427
+ * @param params.pageSize - Number of orders per page (defaults to 10, must be > 0; max 100
428
+ * in page-number mode, up to 1000 when pageToken is present)
429
+ * @param params.pageToken - Pagination cursor: pass `""` to start a cursor walk from
430
+ * the newest (or oldest, for `orderBy: "ASC"`) row; pass a previous response's
431
+ * `nextPageToken` to resume. Any cursor-walk value (`""` or non-empty) ignores `page`
432
+ * and covers full order history (hot and archived rows merged), unlike page-number
433
+ * mode. Omitting both `page` and `pageToken` starts a cursor walk (`pageToken=""`).
427
434
  * @returns Promise resolving to GetPaginatedOrdersResponse with orders and pagination info
428
435
  * @throws {APIError} When API communication fails
429
436
  *
430
437
  * @example
431
438
  * ```typescript
432
- * // Get submitted orders for a specific trading pair with custom pagination
433
- * const orders = await tradingAPI.getPaginatedOrders({
434
- * status: "SUBMITTED",
435
- * tradingPairId: "456e7890-e12b-12d3-a456-426614174000",
436
- * page: 1,
437
- * pageSize: 20
438
- * });
439
- * console.log(`Found ${orders.total} orders, showing page ${orders.page}`);
440
- *
441
- * // Get all orders with default pagination (page=1, pageSize=10)
442
- * const allOrders = await tradingAPI.getPaginatedOrders();
439
+ * // Walk full order history with cursor pagination (the default mode)
440
+ * let pageToken: string | undefined;
441
+ * do {
442
+ * const res = await tradingAPI.getPaginatedOrders({ status: "FILLED", pageToken });
443
+ * handle(res.orders);
444
+ * pageToken = res.nextPageToken;
445
+ * } while (pageToken);
443
446
  * ```
444
447
  */
445
448
  async getPaginatedOrders(params) {
@@ -448,16 +451,25 @@ export class TradingAPIImpl extends BaseAPI {
448
451
  validate(GetPaginatedOrdersSchema, params);
449
452
  }
450
453
  // Set pagination defaults with destructuring and validation
451
- const { page = 1, pageSize: requestedPageSize = 10, status, tradingPairId, tradingMode, marginAccountId, pageToken } = params || {};
452
- const validPage = page > 0 ? page : 1;
454
+ const { page, pageSize: requestedPageSize = 10, status, tradingPairId, tradingMode, marginAccountId, pageToken } = params || {};
453
455
  const validPageSize = requestedPageSize > 0 ? requestedPageSize : 10;
454
- const pageSize = Math.min(validPageSize, 100);
456
+ // Legacy offset mode only when the caller explicitly asked for a page and
457
+ // no cursor was given; everything else drives cursor (pageToken) mode.
458
+ const legacyOffsetMode = page !== undefined && pageToken === undefined;
459
+ // Cursor requests may use pages up to 1000 (keyset reads); page-number
460
+ // requests keep the server's 100 cap.
461
+ const pageSize = Math.min(validPageSize, legacyOffsetMode ? 100 : 1000);
455
462
  const searchParams = new URLSearchParams();
456
- // Always include pagination parameters for consistent API behavior
457
- searchParams.append("page", validPage.toString());
463
+ if (legacyOffsetMode) {
464
+ const validPage = page > 0 ? page : 1;
465
+ searchParams.append("page", validPage.toString());
466
+ }
458
467
  searchParams.append("pageSize", pageSize.toString());
459
- if (status) {
460
- searchParams.append("status", status);
468
+ // An array filter is sent as one comma-separated `status` value, which the
469
+ // API reads as "any of these", so several statuses cost a single request.
470
+ const statusFilter = typeof status === "string" ? status : status?.join(",");
471
+ if (statusFilter) {
472
+ searchParams.append("status", statusFilter);
461
473
  }
462
474
  if (tradingPairId) {
463
475
  searchParams.append("tradingPairId", tradingPairId);
@@ -468,18 +480,55 @@ export class TradingAPIImpl extends BaseAPI {
468
480
  if (marginAccountId) {
469
481
  searchParams.append("marginAccountId", marginAccountId);
470
482
  }
471
- // `!== undefined` (not a truthy check): an explicitly-provided empty
472
- // string is the documented way to start a cursor walk (distinct from
473
- // omitting `pageToken` entirely), so it must still be sent rather than
474
- // silently dropped like a falsy value would be.
475
- if (pageToken !== undefined) {
476
- searchParams.append("pageToken", pageToken);
483
+ // Cursor mode selects itself by the *presence* of `pageToken` on the wire,
484
+ // even when it is the empty string (the documented way to start a walk),
485
+ // so it must be sent explicitly rather than dropped like a falsy value.
486
+ // When the caller gave neither `page` nor `pageToken`, default to starting
487
+ // a cursor walk (`pageToken=""`).
488
+ if (!legacyOffsetMode) {
489
+ searchParams.append("pageToken", pageToken ?? "");
477
490
  }
478
491
  const endpoint = `/api/v1/orders?${searchParams.toString()}`;
479
492
  return await this.makeAuthenticatedRequest(endpoint, {
480
493
  method: "GET",
481
494
  });
482
495
  }
496
+ /**
497
+ * Walks the user's full order history with cursor pagination, yielding one
498
+ * order at a time.
499
+ *
500
+ * Starts a cursor walk (`pageToken: ""`) and follows each response's
501
+ * `nextPageToken` until the server proves exhaustion (a page with zero rows
502
+ * and an empty token). Covers full order history — hot and archived rows
503
+ * merged — unlike the deprecated page-number mode.
504
+ *
505
+ * @param params - Optional filters and page size; `page`/`pageToken` are
506
+ * managed by the iterator and cannot be passed.
507
+ * @returns Async generator yielding orders newest-first (or oldest-first for
508
+ * `orderBy: "ASC"`)
509
+ * @throws {APIError} When any underlying request fails
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * for await (const order of tradingAPI.iterateOrders({ status: "FILLED" })) {
514
+ * console.log(order.id);
515
+ * }
516
+ * ```
517
+ */
518
+ async *iterateOrders(params) {
519
+ let pageToken = "";
520
+ // A non-empty token never guarantees further results (AIP-158 "maybe
521
+ // more"): keep following it until a response returns zero rows and an
522
+ // empty token, the sole proof the walk is exhausted.
523
+ for (;;) {
524
+ const response = await this.getPaginatedOrders({ ...params, pageToken });
525
+ yield* response.orders;
526
+ if (!response.nextPageToken) {
527
+ return;
528
+ }
529
+ pageToken = response.nextPageToken;
530
+ }
531
+ }
483
532
  /**
484
533
  * Gets a single order by its ID.
485
534
  *
@@ -150,6 +150,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
150
150
  * opens. The client signs `WS-AUTH\n<pubkey>\n<ts>` with the session private
151
151
  * key; the server binds the connection to the session. No-op if no session
152
152
  * is set (public channels work unauthenticated).
153
+ *
154
+ * `timestamp` is the current epoch-ms and is bound into the signature; the
155
+ * server rejects it outside a ±30s skew window, so the handshake is freshly
156
+ * signed on every (re)connect rather than cached and replayed.
153
157
  */
154
158
  const sendAuthenticate = () => {
155
159
  if (!session || ws?.readyState !== WebSocket.OPEN)
@@ -504,7 +508,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
504
508
  });
505
509
  };
506
510
  const subscribeMovements = (handler) => {
507
- // User is identified from the JWT token, so channel is just "movements"
511
+ // User is identified from the authenticated session, so channel is just "movements"
508
512
  const channel = "movements";
509
513
  return subscribe(channel, (rawData) => {
510
514
  try {
@@ -541,10 +545,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
541
545
  };
542
546
  /**
543
547
  * Subscribe to authenticated user's order events (all trading pairs)
544
- * Requires authentication - user is identified from the JWT token
548
+ * Requires authentication - user is identified from the signed session-key handshake
545
549
  */
546
550
  const subscribeUserOrders = (handler) => {
547
- // User is identified from the JWT token, so channel is just "orders"
551
+ // User is identified from the authenticated session, so channel is just "orders"
548
552
  const channel = "orders";
549
553
  return subscribe(channel, (rawData) => {
550
554
  try {
@@ -564,10 +568,10 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
564
568
  };
565
569
  /**
566
570
  * Subscribe to authenticated user's balance events
567
- * Requires authentication - user is identified from the JWT token
571
+ * Requires authentication - user is identified from the signed session-key handshake
568
572
  */
569
573
  const subscribeBalances = (handler) => {
570
- // User is identified from the JWT token, so channel is just "balances"
574
+ // User is identified from the authenticated session, so channel is just "balances"
571
575
  const channel = "balances";
572
576
  return subscribe(channel, (rawData) => {
573
577
  try {
@@ -421,7 +421,7 @@ function getSuggestionForConfigField(field) {
421
421
  case "seirpcurl":
422
422
  return "Use 'https://evm-rpc-testnet.sei-apis.com' for testnet or 'https://evm-rpc.sei-apis.com' for mainnet.";
423
423
  case "wsurl":
424
- return "Use 'wss://development.apimonaco.xyz/ws' for testnet or 'wss://api.monaco.xyz/ws' for mainnet.";
424
+ return "Use 'wss://ws-develop.apimonaco.xyz/ws' for testnet or 'wss://ws.apimonaco.xyz/ws' for mainnet.";
425
425
  case "network":
426
426
  return "Valid networks are: 'mainnet', 'development', 'staging', 'local'.";
427
427
  default:
@@ -27,7 +27,9 @@ const DEFAULT_WS_URLS = {
27
27
  // keep working during the migration window.
28
28
  development: "wss://ws-develop.apimonaco.xyz/ws",
29
29
  staging: "wss://ws-staging.apimonaco.xyz/ws",
30
- mainnet: "wss://api.monaco.xyz/ws",
30
+ // wss://api.monaco.xyz/ws never resolved (the monaco.xyz zone has no NS
31
+ // delegation), so this is not a behavior change for any working client.
32
+ mainnet: "wss://ws.apimonaco.xyz/ws",
31
33
  };
32
34
  /**
33
35
  * Resolve the API URL based on network
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/core",
3
- "version": "1.0.29",
3
+ "version": "1.0.32-develop.522174b",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,8 +23,8 @@
23
23
  "viem": "^2.45.2"
24
24
  },
25
25
  "dependencies": {
26
- "@0xmonaco/contracts": "1.0.29",
27
- "@0xmonaco/types": "1.0.29",
26
+ "@0xmonaco/contracts": "1.0.32-develop.522174b",
27
+ "@0xmonaco/types": "1.0.32-develop.522174b",
28
28
  "@noble/curves": "^1.9.1",
29
29
  "@noble/hashes": "^1.8.0",
30
30
  "http-status-codes": "^2.3.0"