@0xmonaco/core 1.0.66 → 1.0.67
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.d.ts +5 -0
- package/dist/api/base.js +26 -111
- package/dist/api/perp/routes.d.ts +2 -0
- package/dist/api/trading/api.d.ts +28 -14
- package/dist/api/trading/api.js +56 -20
- package/dist/api/websocket/websocket.d.ts +6 -1
- package/dist/api/websocket/websocket.js +31 -3
- package/dist/sdk.js +4 -1
- package/dist/utils/http-date.d.ts +14 -0
- package/dist/utils/http-date.js +106 -0
- package/dist/utils/server-clock.d.ts +38 -0
- package/dist/utils/server-clock.js +67 -0
- package/package.json +3 -3
package/dist/api/base.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { RequestSigning, SessionCredentials } from "@0xmonaco/types";
|
|
24
24
|
import { type SessionKeypair } from "../crypto/session";
|
|
25
|
+
import { ServerClock } from "../utils/server-clock";
|
|
25
26
|
export interface RetryOptions {
|
|
26
27
|
maxRetries?: number;
|
|
27
28
|
baseDelayMs?: number;
|
|
@@ -33,9 +34,13 @@ export declare abstract class BaseAPI {
|
|
|
33
34
|
/** Application secret key (`sk_...`) sent in the `x-server-key` header for backend-authenticated requests. */
|
|
34
35
|
protected serverKey?: string;
|
|
35
36
|
private requestSigning;
|
|
37
|
+
/** Server time learned from responses; signing timestamps are read from it. */
|
|
38
|
+
protected serverClock: ServerClock;
|
|
36
39
|
/** Opt in only after the target server supports RFC 9421. No automatic downgrade. */
|
|
37
40
|
setRequestSigning(protocol: RequestSigning): void;
|
|
38
41
|
protected retryOptions: Required<RetryOptions>;
|
|
42
|
+
/** Share one server clock across API instances so any response calibrates all of them. */
|
|
43
|
+
setServerClock(clock: ServerClock): void;
|
|
39
44
|
/**
|
|
40
45
|
* Creates a new BaseAPI instance.
|
|
41
46
|
*
|
package/dist/api/base.js
CHANGED
|
@@ -25,6 +25,8 @@ import { StatusCodes } from "http-status-codes";
|
|
|
25
25
|
import { buildHttpSignatureHeaders } from "../crypto/http-signatures";
|
|
26
26
|
import { composeSigningString, keypairFromHex, sha256Hex, signMessage } from "../crypto/session";
|
|
27
27
|
import { APIError } from "../errors";
|
|
28
|
+
import { parseHttpDate } from "../utils/http-date";
|
|
29
|
+
import { CLOCK_CORRECTION_THRESHOLD_MS, ServerClock } from "../utils/server-clock";
|
|
28
30
|
/**
|
|
29
31
|
* Product token sent as the `User-Agent` on every request. Monaco's edge
|
|
30
32
|
* rejects requests with an empty or absent `User-Agent`, so we set one
|
|
@@ -35,112 +37,6 @@ import { APIError } from "../errors";
|
|
|
35
37
|
* only takes effect in server runtimes (Node/undici, Bun, Deno).
|
|
36
38
|
*/
|
|
37
39
|
const SDK_USER_AGENT = "@0xmonaco/core";
|
|
38
|
-
const MONTHS = {
|
|
39
|
-
Jan: 0,
|
|
40
|
-
Feb: 1,
|
|
41
|
-
Mar: 2,
|
|
42
|
-
Apr: 3,
|
|
43
|
-
May: 4,
|
|
44
|
-
Jun: 5,
|
|
45
|
-
Jul: 6,
|
|
46
|
-
Aug: 7,
|
|
47
|
-
Sep: 8,
|
|
48
|
-
Oct: 9,
|
|
49
|
-
Nov: 10,
|
|
50
|
-
Dec: 11,
|
|
51
|
-
};
|
|
52
|
-
// The three RFC 7231 §7.1.1.1 `HTTP-date` grammars a recipient must accept.
|
|
53
|
-
// Each is anchored so trailing junk is rejected, and `Date.parse` is avoided
|
|
54
|
-
// entirely (it is permissive — it reads asctime in local time and rolls
|
|
55
|
-
// impossible dates like 31 Feb forward instead of rejecting them).
|
|
56
|
-
const MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
|
|
57
|
-
const IMF_FIXDATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (${MONTH}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
|
|
58
|
-
const RFC850_DATE = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d{2})-(${MONTH})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
|
|
59
|
-
const ASCTIME_DATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (${MONTH}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
|
|
60
|
-
/**
|
|
61
|
-
* Expand an RFC 850 two-digit year per RFC 7231 §7.1.1.1: a full timestamp that
|
|
62
|
-
* appears more than 50 years in the future is interpreted as the most recent
|
|
63
|
-
* past year with the same last two digits. The comparison is against the exact
|
|
64
|
-
* receipt instant plus 50 years (not just calendar years), so the boundary can
|
|
65
|
-
* fall mid-year — e.g. received 2026-06-01, `31-Dec-76` is 50y7m ahead and maps
|
|
66
|
-
* to 1976, while `01-Jan-76` is under 50 years ahead and stays 2076.
|
|
67
|
-
*/
|
|
68
|
-
function expandTwoDigitYear(twoDigitYear, month, day, hour, minute, second) {
|
|
69
|
-
const now = new Date(Date.now());
|
|
70
|
-
const candidateYear = Math.floor(now.getUTCFullYear() / 100) * 100 + twoDigitYear;
|
|
71
|
-
const candidate = Date.UTC(candidateYear, month, day, hour, minute, second);
|
|
72
|
-
const fiftyYearsAhead = Date.UTC(now.getUTCFullYear() + 50, now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
|
|
73
|
-
return candidate > fiftyYearsAhead ? candidateYear - 100 : candidateYear;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Parse an RFC 7231 `HTTP-date` — in any of the three formats a recipient must
|
|
77
|
-
* accept (IMF-fixdate, the obsolete RFC 850 form, and asctime) — into a UTC
|
|
78
|
-
* epoch in milliseconds, or `undefined` if the value is not a well-formed, real
|
|
79
|
-
* calendar date. All three denote UTC, so timestamps are built with `Date.UTC`
|
|
80
|
-
* and the fields are round-tripped to reject impossible dates such as
|
|
81
|
-
* `31 Feb` or `25:00:00`. RFC 850's 2-digit year follows the RFC 7231 rule
|
|
82
|
-
* (see {@link expandTwoDigitYear}). The redundant day-name is intentionally not
|
|
83
|
-
* validated against the date — RFC 7231 encourages robust parsing, and the
|
|
84
|
-
* date/time fields already determine the instant unambiguously, so a mismatched
|
|
85
|
-
* weekday still yields the intended delay rather than a dropped header and an
|
|
86
|
-
* arbitrary fallback.
|
|
87
|
-
*/
|
|
88
|
-
function parseHttpDate(value) {
|
|
89
|
-
let day;
|
|
90
|
-
let month;
|
|
91
|
-
let year;
|
|
92
|
-
let hour;
|
|
93
|
-
let minute;
|
|
94
|
-
let second;
|
|
95
|
-
const imf = IMF_FIXDATE.exec(value);
|
|
96
|
-
const rfc850 = RFC850_DATE.exec(value);
|
|
97
|
-
const asctime = ASCTIME_DATE.exec(value);
|
|
98
|
-
if (imf) {
|
|
99
|
-
day = Number(imf[1]);
|
|
100
|
-
month = MONTHS[imf[2]];
|
|
101
|
-
year = Number(imf[3]);
|
|
102
|
-
hour = Number(imf[4]);
|
|
103
|
-
minute = Number(imf[5]);
|
|
104
|
-
second = Number(imf[6]);
|
|
105
|
-
}
|
|
106
|
-
else if (rfc850) {
|
|
107
|
-
day = Number(rfc850[1]);
|
|
108
|
-
month = MONTHS[rfc850[2]];
|
|
109
|
-
hour = Number(rfc850[4]);
|
|
110
|
-
minute = Number(rfc850[5]);
|
|
111
|
-
second = Number(rfc850[6]);
|
|
112
|
-
if (month === undefined)
|
|
113
|
-
return undefined;
|
|
114
|
-
year = expandTwoDigitYear(Number(rfc850[3]), month, day, hour, minute, second);
|
|
115
|
-
}
|
|
116
|
-
else if (asctime) {
|
|
117
|
-
month = MONTHS[asctime[1]];
|
|
118
|
-
day = Number((asctime[2] ?? "").trim());
|
|
119
|
-
hour = Number(asctime[3]);
|
|
120
|
-
minute = Number(asctime[4]);
|
|
121
|
-
second = Number(asctime[5]);
|
|
122
|
-
year = Number(asctime[6]);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
if (month === undefined)
|
|
128
|
-
return undefined;
|
|
129
|
-
const timestamp = Date.UTC(year, month, day, hour, minute, second);
|
|
130
|
-
const utc = new Date(timestamp);
|
|
131
|
-
// Reject impossible calendar/clock values that Date.UTC silently rolls over
|
|
132
|
-
// (e.g. 31 Feb, 25:00:00). A `:60` leap second is likewise dropped rather
|
|
133
|
-
// than special-cased — leap seconds never appear in a Retry-After header.
|
|
134
|
-
if (utc.getUTCFullYear() !== year ||
|
|
135
|
-
utc.getUTCMonth() !== month ||
|
|
136
|
-
utc.getUTCDate() !== day ||
|
|
137
|
-
utc.getUTCHours() !== hour ||
|
|
138
|
-
utc.getUTCMinutes() !== minute ||
|
|
139
|
-
utc.getUTCSeconds() !== second) {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
return timestamp;
|
|
143
|
-
}
|
|
144
40
|
export class BaseAPI {
|
|
145
41
|
apiUrl;
|
|
146
42
|
/** Active session keypair (raw bytes) used to sign authenticated requests. */
|
|
@@ -148,6 +44,8 @@ export class BaseAPI {
|
|
|
148
44
|
/** Application secret key (`sk_...`) sent in the `x-server-key` header for backend-authenticated requests. */
|
|
149
45
|
serverKey;
|
|
150
46
|
requestSigning = "legacy";
|
|
47
|
+
/** Server time learned from responses; signing timestamps are read from it. */
|
|
48
|
+
serverClock = new ServerClock();
|
|
151
49
|
/** Opt in only after the target server supports RFC 9421. No automatic downgrade. */
|
|
152
50
|
setRequestSigning(protocol) {
|
|
153
51
|
if (protocol !== "legacy" && protocol !== "rfc9421")
|
|
@@ -155,6 +53,10 @@ export class BaseAPI {
|
|
|
155
53
|
this.requestSigning = protocol;
|
|
156
54
|
}
|
|
157
55
|
retryOptions;
|
|
56
|
+
/** Share one server clock across API instances so any response calibrates all of them. */
|
|
57
|
+
setServerClock(clock) {
|
|
58
|
+
this.serverClock = clock;
|
|
59
|
+
}
|
|
158
60
|
/**
|
|
159
61
|
* Creates a new BaseAPI instance.
|
|
160
62
|
*
|
|
@@ -293,6 +195,7 @@ export class BaseAPI {
|
|
|
293
195
|
requestBody,
|
|
294
196
|
});
|
|
295
197
|
}
|
|
198
|
+
this.serverClock.observeResponse(response.headers, Date.now());
|
|
296
199
|
// Parse response body with error handling for non-JSON responses
|
|
297
200
|
let responseBody;
|
|
298
201
|
const contentType = response.headers?.get("content-type") || "unknown";
|
|
@@ -396,7 +299,6 @@ export class BaseAPI {
|
|
|
396
299
|
}
|
|
397
300
|
const url = `${this.apiUrl}${endpoint}`;
|
|
398
301
|
const requestBody = this.parseRequestBody(options.body);
|
|
399
|
-
const signatureHeaders = this.buildSignatureHeaders(endpoint, options);
|
|
400
302
|
let extraHeaders = options.headers;
|
|
401
303
|
if (this.requestSigning === "rfc9421") {
|
|
402
304
|
const normalized = {};
|
|
@@ -405,15 +307,28 @@ export class BaseAPI {
|
|
|
405
307
|
});
|
|
406
308
|
extraHeaders = normalized;
|
|
407
309
|
}
|
|
408
|
-
|
|
310
|
+
const send = () => this.executeRequest(url, endpoint, {
|
|
409
311
|
...options,
|
|
410
312
|
...(this.requestSigning === "rfc9421" ? { redirect: "error" } : {}),
|
|
411
313
|
headers: {
|
|
412
314
|
"Content-Type": "application/json",
|
|
413
315
|
...extraHeaders,
|
|
414
|
-
...
|
|
316
|
+
...this.buildSignatureHeaders(endpoint, options),
|
|
415
317
|
},
|
|
416
318
|
}, requestBody);
|
|
319
|
+
// A 401 whose response moved the server clock was most likely a
|
|
320
|
+
// timestamp-skew rejection: re-sign once with the corrected time.
|
|
321
|
+
const offsetBefore = this.serverClock.offsetMs;
|
|
322
|
+
try {
|
|
323
|
+
return await send();
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const clockMoved = Math.abs(this.serverClock.offsetMs - offsetBefore) >= CLOCK_CORRECTION_THRESHOLD_MS;
|
|
327
|
+
if (error instanceof APIError && error.statusCode === StatusCodes.UNAUTHORIZED && clockMoved) {
|
|
328
|
+
return send();
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
417
332
|
}
|
|
418
333
|
/**
|
|
419
334
|
* Compute the per-request ed25519 signature headers.
|
|
@@ -441,9 +356,9 @@ export class BaseAPI {
|
|
|
441
356
|
throw new Error("Signature headers are managed by the SDK");
|
|
442
357
|
}
|
|
443
358
|
const url = new URL(`${this.apiUrl}${endpoint}`);
|
|
444
|
-
return buildHttpSignatureHeaders(this.sessionKeypair, method, url.pathname + url.search, utf8ToBytes(options.body ?? ""), Math.floor(
|
|
359
|
+
return buildHttpSignatureHeaders(this.sessionKeypair, method, url.pathname + url.search, utf8ToBytes(options.body ?? ""), Math.floor(this.serverClock.now() / 1000));
|
|
445
360
|
}
|
|
446
|
-
const timestampMs =
|
|
361
|
+
const timestampMs = this.serverClock.now();
|
|
447
362
|
const bodyBytes = typeof options.body === "string" ? utf8ToBytes(options.body) : new Uint8Array(0);
|
|
448
363
|
const bodyHash = sha256Hex(bodyBytes);
|
|
449
364
|
const signingString = composeSigningString(method, endpoint, timestampMs, bodyHash);
|
|
@@ -23,6 +23,7 @@ export declare const perpRoutes: {
|
|
|
23
23
|
state?: string;
|
|
24
24
|
page?: number;
|
|
25
25
|
pageSize?: number;
|
|
26
|
+
pageToken?: string;
|
|
26
27
|
}) => string;
|
|
27
28
|
readonly cancelConditional: (conditionalOrderId: string) => string;
|
|
28
29
|
readonly getConditional: (conditionalOrderId: string) => string;
|
|
@@ -32,6 +33,7 @@ export declare const perpRoutes: {
|
|
|
32
33
|
tradingPairId?: string;
|
|
33
34
|
page?: number;
|
|
34
35
|
pageSize?: number;
|
|
36
|
+
pageToken?: string;
|
|
35
37
|
}) => string;
|
|
36
38
|
readonly getTwap: (twapOrderId: string) => string;
|
|
37
39
|
readonly getTwapTca: (twapOrderId: string) => string;
|
|
@@ -191,13 +191,20 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
|
|
|
191
191
|
/**
|
|
192
192
|
* Lists conditional orders for the authenticated user.
|
|
193
193
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
194
|
+
* Cursor (pageToken) pagination is the default: when neither `page` nor
|
|
195
|
+
* `pageToken` is passed, the SDK drives a cursor walk (it sends
|
|
196
|
+
* `pageToken=""`), which seeks on `(createdAt, id)` newest-first and resumes
|
|
197
|
+
* via each response's `nextPageToken` until that token comes back empty. A
|
|
198
|
+
* cursor page size may go up to 1000.
|
|
199
|
+
*
|
|
200
|
+
* Passing the deprecated `page` (without `pageToken`) selects legacy
|
|
201
|
+
* page-number mode instead, which keeps the 100 page-size cap and populates a
|
|
202
|
+
* bounded `total` (exact up to `pageSize` x 10,000 rows, saturating there);
|
|
203
|
+
* `page` is capped at 10,000. Cursor mode returns `total`/`page` as 0 and runs
|
|
204
|
+
* no COUNT.
|
|
205
|
+
*
|
|
206
|
+
* @param params - Optional filters and pagination; `pageToken` drives cursor
|
|
207
|
+
* mode, `page` drives the deprecated legacy mode
|
|
201
208
|
* @returns Promise resolving to ListConditionalOrdersResponse with orders and pagination info
|
|
202
209
|
* @throws {APIError} When API communication fails
|
|
203
210
|
*/
|
|
@@ -522,20 +529,27 @@ export declare class TradingAPIImpl extends BaseAPI implements TradingAPI {
|
|
|
522
529
|
/**
|
|
523
530
|
* Lists TWAP parent orders for the authenticated user.
|
|
524
531
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
532
|
+
* Cursor (pageToken) pagination is the default: when neither `page` nor
|
|
533
|
+
* `pageToken` is passed, the SDK drives a cursor walk (it sends
|
|
534
|
+
* `pageToken=""`), which seeks on `(createdAt, id)` newest-first and resumes
|
|
535
|
+
* via each response's `nextPageToken` until that token comes back empty. A
|
|
536
|
+
* cursor page size may go up to 1000.
|
|
537
|
+
*
|
|
538
|
+
* Passing the deprecated `page` (without `pageToken`) selects legacy
|
|
539
|
+
* page-number mode instead, which keeps the 100 page-size cap and populates a
|
|
540
|
+
* bounded `total` (exact up to `pageSize` x 10,000 rows, saturating there);
|
|
541
|
+
* `page` is capped at 10,000. Cursor mode returns `total`/`page` as 0 and runs
|
|
542
|
+
* no COUNT.
|
|
530
543
|
*
|
|
531
|
-
* @param params - Optional
|
|
544
|
+
* @param params - Optional filters and pagination; `pageToken` drives cursor
|
|
545
|
+
* mode, `page` drives the deprecated legacy mode
|
|
532
546
|
* @returns Promise resolving to ListTwapOrdersResponse with orders and pagination info
|
|
533
547
|
* @throws {APIError} When API communication fails
|
|
534
548
|
*
|
|
535
549
|
* @example
|
|
536
550
|
* ```typescript
|
|
537
551
|
* const active = await tradingAPI.listTwapOrders({ state: "ACTIVE", pageSize: 50 });
|
|
538
|
-
* console.log(`Found ${active.
|
|
552
|
+
* console.log(`Found ${active.orders.length} active TWAP orders`);
|
|
539
553
|
* ```
|
|
540
554
|
*/
|
|
541
555
|
listTwapOrders(params?: ListTwapOrdersParams): Promise<ListTwapOrdersResponse>;
|
package/dist/api/trading/api.js
CHANGED
|
@@ -276,13 +276,20 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
276
276
|
/**
|
|
277
277
|
* Lists conditional orders for the authenticated user.
|
|
278
278
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
279
|
+
* Cursor (pageToken) pagination is the default: when neither `page` nor
|
|
280
|
+
* `pageToken` is passed, the SDK drives a cursor walk (it sends
|
|
281
|
+
* `pageToken=""`), which seeks on `(createdAt, id)` newest-first and resumes
|
|
282
|
+
* via each response's `nextPageToken` until that token comes back empty. A
|
|
283
|
+
* cursor page size may go up to 1000.
|
|
284
|
+
*
|
|
285
|
+
* Passing the deprecated `page` (without `pageToken`) selects legacy
|
|
286
|
+
* page-number mode instead, which keeps the 100 page-size cap and populates a
|
|
287
|
+
* bounded `total` (exact up to `pageSize` x 10,000 rows, saturating there);
|
|
288
|
+
* `page` is capped at 10,000. Cursor mode returns `total`/`page` as 0 and runs
|
|
289
|
+
* no COUNT.
|
|
290
|
+
*
|
|
291
|
+
* @param params - Optional filters and pagination; `pageToken` drives cursor
|
|
292
|
+
* mode, `page` drives the deprecated legacy mode
|
|
286
293
|
* @returns Promise resolving to ListConditionalOrdersResponse with orders and pagination info
|
|
287
294
|
* @throws {APIError} When API communication fails
|
|
288
295
|
*/
|
|
@@ -290,13 +297,24 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
290
297
|
if (params) {
|
|
291
298
|
validate(ListConditionalOrdersSchema, params);
|
|
292
299
|
}
|
|
293
|
-
const { page
|
|
300
|
+
const { page, pageSize = 20, marginAccountId, tradingPairId, state, pageToken } = params || {};
|
|
301
|
+
// Legacy offset mode only when the caller explicitly asked for a page and no
|
|
302
|
+
// cursor was given; everything else drives cursor (pageToken) mode.
|
|
303
|
+
const legacyOffsetMode = page !== undefined && pageToken === undefined;
|
|
304
|
+
// Cursor requests may use pages up to 1000 (keyset reads); page-number
|
|
305
|
+
// requests keep the server's 100 cap.
|
|
306
|
+
const boundedPageSize = Math.min(Math.max(pageSize, 1), legacyOffsetMode ? 100 : 1000);
|
|
294
307
|
return await this.makeAuthenticatedRequest(perpRoutes.orders.listConditional({
|
|
295
|
-
page,
|
|
296
|
-
pageSize: Math.min(Math.max(pageSize, 1), 100),
|
|
297
308
|
marginAccountId,
|
|
298
309
|
tradingPairId,
|
|
299
310
|
state,
|
|
311
|
+
// Legacy mode sends `page` and omits `pageToken`; cursor mode omits
|
|
312
|
+
// `page` and sends `pageToken` — present even as the empty string, the
|
|
313
|
+
// documented way to start a walk (the server selects cursor mode by the
|
|
314
|
+
// token's presence, not its value).
|
|
315
|
+
page: legacyOffsetMode ? page : undefined,
|
|
316
|
+
pageSize: boundedPageSize,
|
|
317
|
+
pageToken: legacyOffsetMode ? undefined : (pageToken ?? ""),
|
|
300
318
|
}), { method: "GET" });
|
|
301
319
|
}
|
|
302
320
|
/**
|
|
@@ -821,32 +839,50 @@ export class TradingAPIImpl extends BaseAPI {
|
|
|
821
839
|
/**
|
|
822
840
|
* Lists TWAP parent orders for the authenticated user.
|
|
823
841
|
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
842
|
+
* Cursor (pageToken) pagination is the default: when neither `page` nor
|
|
843
|
+
* `pageToken` is passed, the SDK drives a cursor walk (it sends
|
|
844
|
+
* `pageToken=""`), which seeks on `(createdAt, id)` newest-first and resumes
|
|
845
|
+
* via each response's `nextPageToken` until that token comes back empty. A
|
|
846
|
+
* cursor page size may go up to 1000.
|
|
829
847
|
*
|
|
830
|
-
*
|
|
848
|
+
* Passing the deprecated `page` (without `pageToken`) selects legacy
|
|
849
|
+
* page-number mode instead, which keeps the 100 page-size cap and populates a
|
|
850
|
+
* bounded `total` (exact up to `pageSize` x 10,000 rows, saturating there);
|
|
851
|
+
* `page` is capped at 10,000. Cursor mode returns `total`/`page` as 0 and runs
|
|
852
|
+
* no COUNT.
|
|
853
|
+
*
|
|
854
|
+
* @param params - Optional filters and pagination; `pageToken` drives cursor
|
|
855
|
+
* mode, `page` drives the deprecated legacy mode
|
|
831
856
|
* @returns Promise resolving to ListTwapOrdersResponse with orders and pagination info
|
|
832
857
|
* @throws {APIError} When API communication fails
|
|
833
858
|
*
|
|
834
859
|
* @example
|
|
835
860
|
* ```typescript
|
|
836
861
|
* const active = await tradingAPI.listTwapOrders({ state: "ACTIVE", pageSize: 50 });
|
|
837
|
-
* console.log(`Found ${active.
|
|
862
|
+
* console.log(`Found ${active.orders.length} active TWAP orders`);
|
|
838
863
|
* ```
|
|
839
864
|
*/
|
|
840
865
|
async listTwapOrders(params) {
|
|
841
866
|
if (params) {
|
|
842
867
|
validate(ListTwapOrdersSchema, params);
|
|
843
868
|
}
|
|
844
|
-
const { page
|
|
869
|
+
const { page, pageSize = 20, state, tradingPairId, pageToken } = params || {};
|
|
870
|
+
// Legacy offset mode only when the caller explicitly asked for a page and no
|
|
871
|
+
// cursor was given; everything else drives cursor (pageToken) mode.
|
|
872
|
+
const legacyOffsetMode = page !== undefined && pageToken === undefined;
|
|
873
|
+
// Cursor requests may use pages up to 1000 (keyset reads); page-number
|
|
874
|
+
// requests keep the server's 100 cap.
|
|
875
|
+
const boundedPageSize = Math.min(Math.max(pageSize, 1), legacyOffsetMode ? 100 : 1000);
|
|
845
876
|
return await this.makeAuthenticatedRequest(perpRoutes.orders.listTwap({
|
|
846
|
-
page,
|
|
847
|
-
pageSize: Math.min(Math.max(pageSize, 1), 100),
|
|
848
877
|
state,
|
|
849
878
|
tradingPairId,
|
|
879
|
+
// Legacy mode sends `page` and omits `pageToken`; cursor mode omits
|
|
880
|
+
// `page` and sends `pageToken` — present even as the empty string, the
|
|
881
|
+
// documented way to start a walk (the server selects cursor mode by the
|
|
882
|
+
// token's presence, not its value).
|
|
883
|
+
page: legacyOffsetMode ? page : undefined,
|
|
884
|
+
pageSize: boundedPageSize,
|
|
885
|
+
pageToken: legacyOffsetMode ? undefined : (pageToken ?? ""),
|
|
850
886
|
}), { method: "GET" });
|
|
851
887
|
}
|
|
852
888
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { ServerClock } from "../../utils/server-clock";
|
|
1
2
|
import type { MonacoWebSocket, MonacoWebSocketOptions } from "./types";
|
|
2
3
|
/**
|
|
3
4
|
* Create a Monaco WebSocket client
|
|
5
|
+
*
|
|
6
|
+
* @param serverClock - Server time used to stamp the auth handshake; the SDK
|
|
7
|
+
* passes the clock its REST clients calibrate so a skewed device clock does
|
|
8
|
+
* not fail authentication.
|
|
4
9
|
*/
|
|
5
|
-
export declare function createMonacoWebSocket(baseUrl: string, options?: MonacoWebSocketOptions): MonacoWebSocket;
|
|
10
|
+
export declare function createMonacoWebSocket(baseUrl: string, options?: MonacoWebSocketOptions, serverClock?: ServerClock): MonacoWebSocket;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
2
2
|
import { composeWsSigningString, signMessage } from "../../crypto/session";
|
|
3
3
|
import { ALL_MAGNITUDES } from "../../utils";
|
|
4
|
+
import { ServerClock } from "../../utils/server-clock";
|
|
4
5
|
import { keysToCamelCase } from "./utils";
|
|
5
6
|
// Connection constants
|
|
6
7
|
const CONNECTION_TIMEOUT = 10000;
|
|
7
8
|
const HEARTBEAT_INTERVAL = 15000;
|
|
8
9
|
const MAX_RECONNECT_DELAY = 30000;
|
|
9
10
|
const RECONNECT_BASE_DELAY = 1000;
|
|
11
|
+
/**
|
|
12
|
+
* Handshakes rejected for timestamp skew that are retried before the session
|
|
13
|
+
* is treated as lost. A restored session can authenticate before any REST
|
|
14
|
+
* response has calibrated the server clock; each retry re-signs with
|
|
15
|
+
* whatever the clock has learned since.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_SKEW_AUTH_RETRIES = 3;
|
|
18
|
+
const SKEW_REJECTION = /timestamp outside allowed skew/i;
|
|
10
19
|
function isRecord(value) {
|
|
11
20
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
21
|
}
|
|
@@ -165,6 +174,11 @@ function parseConditionalOrderEventData(raw) {
|
|
|
165
174
|
// Free-form on the wire (the server field is a plain string), so it is
|
|
166
175
|
// passed through untouched rather than matched against a value list.
|
|
167
176
|
reason: readString(data, "reason"),
|
|
177
|
+
// Sent on every frame by current servers, but read optionally: a frame
|
|
178
|
+
// from an older deployment during a rolling release omits it, and a
|
|
179
|
+
// `readString` there would throw inside the message handler and drop the
|
|
180
|
+
// update.
|
|
181
|
+
createdAt: readOptionalString(data, "createdAt"),
|
|
168
182
|
updatedAt: readString(data, "updatedAt"),
|
|
169
183
|
// Populated on the subscribe-time snapshot frame and absent on live
|
|
170
184
|
// updates (0XM-2440). Reading it optionally is what keeps the snapshot's
|
|
@@ -669,8 +683,12 @@ function snapshotFrameHandler(channel, parseItem, onSnapshot) {
|
|
|
669
683
|
}
|
|
670
684
|
/**
|
|
671
685
|
* Create a Monaco WebSocket client
|
|
686
|
+
*
|
|
687
|
+
* @param serverClock - Server time used to stamp the auth handshake; the SDK
|
|
688
|
+
* passes the clock its REST clients calibrate so a skewed device clock does
|
|
689
|
+
* not fail authentication.
|
|
672
690
|
*/
|
|
673
|
-
export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
691
|
+
export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new ServerClock()) {
|
|
674
692
|
let ws = null;
|
|
675
693
|
let session = options.session;
|
|
676
694
|
let reconnectAttempts = 0;
|
|
@@ -698,6 +716,8 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
698
716
|
// connection never decides how a later connection's close is read.
|
|
699
717
|
let lastAuthError = null;
|
|
700
718
|
let pendingAuthSessions = [];
|
|
719
|
+
// Consecutive handshakes rejected for clock skew; reset on success.
|
|
720
|
+
let skewAuthRetries = 0;
|
|
701
721
|
let confirmedSession;
|
|
702
722
|
// Handler storage: channel -> set of handlers (supports multiple subscribers per channel)
|
|
703
723
|
const handlers = new Map();
|
|
@@ -746,7 +766,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
746
766
|
const sendAuthenticate = () => {
|
|
747
767
|
if (!session || ws?.readyState !== WebSocket.OPEN)
|
|
748
768
|
return;
|
|
749
|
-
const timestamp =
|
|
769
|
+
const timestamp = serverClock.now();
|
|
750
770
|
const signature = signMessage(hexToBytes(session.privateKey), composeWsSigningString(session.publicKey, timestamp));
|
|
751
771
|
pendingAuthSessions.push(session);
|
|
752
772
|
ws.send(JSON.stringify({
|
|
@@ -881,6 +901,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
881
901
|
}
|
|
882
902
|
if (msg.type === "Authenticated") {
|
|
883
903
|
confirmedSession = pendingAuthSessions.shift();
|
|
904
|
+
skewAuthRetries = 0;
|
|
884
905
|
return;
|
|
885
906
|
}
|
|
886
907
|
if (!msg.channel)
|
|
@@ -1000,7 +1021,13 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1000
1021
|
// (e.g. re-authenticating from onError): the fresh session is what
|
|
1001
1022
|
// the reconnect uses.
|
|
1002
1023
|
const authError = lastAuthError;
|
|
1003
|
-
|
|
1024
|
+
// A handshake stamped outside the server's window is a clock
|
|
1025
|
+
// problem, not a dead session: reconnect and re-sign, a bounded
|
|
1026
|
+
// number of times, instead of discarding the credentials.
|
|
1027
|
+
const skewRetry = autoReconnect && authError?.code === "AUTH_FAILED" && SKEW_REJECTION.test(authError.message) && skewAuthRetries < MAX_SKEW_AUTH_RETRIES;
|
|
1028
|
+
if (skewRetry)
|
|
1029
|
+
skewAuthRetries++;
|
|
1030
|
+
const authLoss = !skewRetry && authError !== null && authError.session !== undefined && (authError.code === "AUTH_FAILED" || event.code === 1008);
|
|
1004
1031
|
const terminalAuthLoss = authLoss && session === authError.session;
|
|
1005
1032
|
if (terminalAuthLoss) {
|
|
1006
1033
|
session = undefined;
|
|
@@ -1665,6 +1692,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1665
1692
|
getStatus,
|
|
1666
1693
|
setSessionKeypair: (credentials) => {
|
|
1667
1694
|
session = credentials ?? undefined;
|
|
1695
|
+
skewAuthRetries = 0;
|
|
1668
1696
|
// If already connected, (re)send the signed handshake so the connection
|
|
1669
1697
|
// picks up the new session without tearing down. If not yet connected,
|
|
1670
1698
|
// onopen will send it. Clearing the session leaves the connection up for
|
package/dist/sdk.js
CHANGED
|
@@ -24,6 +24,7 @@ import { WithdrawalsAPIImpl } from "./api/withdrawals";
|
|
|
24
24
|
import { generateSessionKeypair, privateKeyHex, publicKeyHex } from "./crypto/session";
|
|
25
25
|
import { APIError, InvalidConfigError, InvalidStateError } from "./errors";
|
|
26
26
|
import { resolveApiUrl, resolveWsUrl } from "./networks";
|
|
27
|
+
import { ServerClock } from "./utils/server-clock";
|
|
27
28
|
/**
|
|
28
29
|
* Default receipt-polling frequency for the SDK's internal viem public client.
|
|
29
30
|
* Sei produces sub-second blocks, so viem's 4000ms default adds seconds of
|
|
@@ -189,8 +190,10 @@ export class MonacoSDKImpl {
|
|
|
189
190
|
this.walletClient = cfg.walletClient;
|
|
190
191
|
}
|
|
191
192
|
// Instantiate APIs (wallet-dependent APIs will be initialized lazily or error if wallet not set)
|
|
193
|
+
const serverClock = new ServerClock();
|
|
192
194
|
const configureSigning = (api) => {
|
|
193
195
|
api.setRequestSigning(cfg.requestSigning ?? "legacy");
|
|
196
|
+
api.setServerClock(serverClock);
|
|
194
197
|
return api;
|
|
195
198
|
};
|
|
196
199
|
this.applications = configureSigning(new ApplicationsAPIImpl(apiUrl));
|
|
@@ -218,7 +221,7 @@ export class MonacoSDKImpl {
|
|
|
218
221
|
// Thread the reconnect/resync knobs and callbacks from SDK config into the
|
|
219
222
|
// internally-built ws client. The session keypair is not passed here — it
|
|
220
223
|
// is propagated on login via propagateSession/setSessionKeypair.
|
|
221
|
-
this.ws = createMonacoWebSocket(wsUrl, cfg.ws);
|
|
224
|
+
this.ws = createMonacoWebSocket(wsUrl, cfg.ws, serverClock);
|
|
222
225
|
// Auto-connect WebSocket, log errors
|
|
223
226
|
this.ws.connect().catch(console.error);
|
|
224
227
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an RFC 7231 `HTTP-date` — in any of the three formats a recipient must
|
|
3
|
+
* accept (IMF-fixdate, the obsolete RFC 850 form, and asctime) — into a UTC
|
|
4
|
+
* epoch in milliseconds, or `undefined` if the value is not a well-formed, real
|
|
5
|
+
* calendar date. All three denote UTC, so timestamps are built with `Date.UTC`
|
|
6
|
+
* and the fields are round-tripped to reject impossible dates such as
|
|
7
|
+
* `31 Feb` or `25:00:00`. RFC 850's 2-digit year follows the RFC 7231 rule
|
|
8
|
+
* (see {@link expandTwoDigitYear}). The redundant day-name is intentionally not
|
|
9
|
+
* validated against the date — RFC 7231 encourages robust parsing, and the
|
|
10
|
+
* date/time fields already determine the instant unambiguously, so a mismatched
|
|
11
|
+
* weekday still yields the intended delay rather than a dropped header and an
|
|
12
|
+
* arbitrary fallback.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseHttpDate(value: string): number | undefined;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const MONTHS = {
|
|
2
|
+
Jan: 0,
|
|
3
|
+
Feb: 1,
|
|
4
|
+
Mar: 2,
|
|
5
|
+
Apr: 3,
|
|
6
|
+
May: 4,
|
|
7
|
+
Jun: 5,
|
|
8
|
+
Jul: 6,
|
|
9
|
+
Aug: 7,
|
|
10
|
+
Sep: 8,
|
|
11
|
+
Oct: 9,
|
|
12
|
+
Nov: 10,
|
|
13
|
+
Dec: 11,
|
|
14
|
+
};
|
|
15
|
+
// The three RFC 7231 §7.1.1.1 `HTTP-date` grammars a recipient must accept.
|
|
16
|
+
// Each is anchored so trailing junk is rejected, and `Date.parse` is avoided
|
|
17
|
+
// entirely (it is permissive — it reads asctime in local time and rolls
|
|
18
|
+
// impossible dates like 31 Feb forward instead of rejecting them).
|
|
19
|
+
const MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
|
|
20
|
+
const IMF_FIXDATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (${MONTH}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
|
|
21
|
+
const RFC850_DATE = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d{2})-(${MONTH})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
|
|
22
|
+
const ASCTIME_DATE = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (${MONTH}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
|
|
23
|
+
/**
|
|
24
|
+
* Expand an RFC 850 two-digit year per RFC 7231 §7.1.1.1: a full timestamp that
|
|
25
|
+
* appears more than 50 years in the future is interpreted as the most recent
|
|
26
|
+
* past year with the same last two digits. The comparison is against the exact
|
|
27
|
+
* receipt instant plus 50 years (not just calendar years), so the boundary can
|
|
28
|
+
* fall mid-year — e.g. received 2026-06-01, `31-Dec-76` is 50y7m ahead and maps
|
|
29
|
+
* to 1976, while `01-Jan-76` is under 50 years ahead and stays 2076.
|
|
30
|
+
*/
|
|
31
|
+
function expandTwoDigitYear(twoDigitYear, month, day, hour, minute, second) {
|
|
32
|
+
const now = new Date(Date.now());
|
|
33
|
+
const candidateYear = Math.floor(now.getUTCFullYear() / 100) * 100 + twoDigitYear;
|
|
34
|
+
const candidate = Date.UTC(candidateYear, month, day, hour, minute, second);
|
|
35
|
+
const fiftyYearsAhead = Date.UTC(now.getUTCFullYear() + 50, now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
|
|
36
|
+
return candidate > fiftyYearsAhead ? candidateYear - 100 : candidateYear;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse an RFC 7231 `HTTP-date` — in any of the three formats a recipient must
|
|
40
|
+
* accept (IMF-fixdate, the obsolete RFC 850 form, and asctime) — into a UTC
|
|
41
|
+
* epoch in milliseconds, or `undefined` if the value is not a well-formed, real
|
|
42
|
+
* calendar date. All three denote UTC, so timestamps are built with `Date.UTC`
|
|
43
|
+
* and the fields are round-tripped to reject impossible dates such as
|
|
44
|
+
* `31 Feb` or `25:00:00`. RFC 850's 2-digit year follows the RFC 7231 rule
|
|
45
|
+
* (see {@link expandTwoDigitYear}). The redundant day-name is intentionally not
|
|
46
|
+
* validated against the date — RFC 7231 encourages robust parsing, and the
|
|
47
|
+
* date/time fields already determine the instant unambiguously, so a mismatched
|
|
48
|
+
* weekday still yields the intended delay rather than a dropped header and an
|
|
49
|
+
* arbitrary fallback.
|
|
50
|
+
*/
|
|
51
|
+
export function parseHttpDate(value) {
|
|
52
|
+
let day;
|
|
53
|
+
let month;
|
|
54
|
+
let year;
|
|
55
|
+
let hour;
|
|
56
|
+
let minute;
|
|
57
|
+
let second;
|
|
58
|
+
const imf = IMF_FIXDATE.exec(value);
|
|
59
|
+
const rfc850 = RFC850_DATE.exec(value);
|
|
60
|
+
const asctime = ASCTIME_DATE.exec(value);
|
|
61
|
+
if (imf) {
|
|
62
|
+
day = Number(imf[1]);
|
|
63
|
+
month = MONTHS[imf[2]];
|
|
64
|
+
year = Number(imf[3]);
|
|
65
|
+
hour = Number(imf[4]);
|
|
66
|
+
minute = Number(imf[5]);
|
|
67
|
+
second = Number(imf[6]);
|
|
68
|
+
}
|
|
69
|
+
else if (rfc850) {
|
|
70
|
+
day = Number(rfc850[1]);
|
|
71
|
+
month = MONTHS[rfc850[2]];
|
|
72
|
+
hour = Number(rfc850[4]);
|
|
73
|
+
minute = Number(rfc850[5]);
|
|
74
|
+
second = Number(rfc850[6]);
|
|
75
|
+
if (month === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
year = expandTwoDigitYear(Number(rfc850[3]), month, day, hour, minute, second);
|
|
78
|
+
}
|
|
79
|
+
else if (asctime) {
|
|
80
|
+
month = MONTHS[asctime[1]];
|
|
81
|
+
day = Number((asctime[2] ?? "").trim());
|
|
82
|
+
hour = Number(asctime[3]);
|
|
83
|
+
minute = Number(asctime[4]);
|
|
84
|
+
second = Number(asctime[5]);
|
|
85
|
+
year = Number(asctime[6]);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (month === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const timestamp = Date.UTC(year, month, day, hour, minute, second);
|
|
93
|
+
const utc = new Date(timestamp);
|
|
94
|
+
// Reject impossible calendar/clock values that Date.UTC silently rolls over
|
|
95
|
+
// (e.g. 31 Feb, 25:00:00). A `:60` leap second is likewise dropped rather
|
|
96
|
+
// than special-cased — leap seconds never appear in a Retry-After header.
|
|
97
|
+
if (utc.getUTCFullYear() !== year ||
|
|
98
|
+
utc.getUTCMonth() !== month ||
|
|
99
|
+
utc.getUTCDate() !== day ||
|
|
100
|
+
utc.getUTCHours() !== hour ||
|
|
101
|
+
utc.getUTCMinutes() !== minute ||
|
|
102
|
+
utc.getUTCSeconds() !== second) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return timestamp;
|
|
106
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offsets below this are left uncorrected: the server accepts signatures
|
|
3
|
+
* within ±30s, and the `Date` header only has whole-second resolution, so
|
|
4
|
+
* correcting a nearly-right clock would add noise without fixing anything.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CLOCK_CORRECTION_THRESHOLD_MS = 5000;
|
|
7
|
+
/**
|
|
8
|
+
* The server's notion of "now", learned from API response `Date` headers.
|
|
9
|
+
*
|
|
10
|
+
* Request signatures carry a timestamp the server rejects outside a ±30s
|
|
11
|
+
* window, so a device whose clock is minutes off fails every signed call.
|
|
12
|
+
* Each response reports the server's time; the offset from the local clock
|
|
13
|
+
* is applied to signing timestamps once it exceeds
|
|
14
|
+
* {@link CLOCK_CORRECTION_THRESHOLD_MS}.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ServerClock {
|
|
17
|
+
private offset;
|
|
18
|
+
/** Milliseconds added to the local clock; 0 while the device clock is close enough. */
|
|
19
|
+
get offsetMs(): number;
|
|
20
|
+
/** Current epoch milliseconds on the server's clock. */
|
|
21
|
+
now(): number;
|
|
22
|
+
/**
|
|
23
|
+
* Record the server time from a response, skipping any the browser may
|
|
24
|
+
* have served from its HTTP cache: a cached copy keeps its original `Date`,
|
|
25
|
+
* which can be older than the ±30s signing window.
|
|
26
|
+
*/
|
|
27
|
+
observeResponse(headers: Headers | undefined, receivedAtMs: number): void;
|
|
28
|
+
/**
|
|
29
|
+
* Record the server time from a response `Date` header.
|
|
30
|
+
*
|
|
31
|
+
* The server stamps `Date` when it sends the response, so it is compared
|
|
32
|
+
* with the local receipt time; request upload and server processing do not
|
|
33
|
+
* skew the estimate. The header is truncated to the second, so it is read
|
|
34
|
+
* as the middle of that second. A missing or malformed header leaves the
|
|
35
|
+
* offset unchanged.
|
|
36
|
+
*/
|
|
37
|
+
observe(dateHeader: string | null | undefined, receivedAtMs: number): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseHttpDate } from "./http-date";
|
|
2
|
+
/**
|
|
3
|
+
* Offsets below this are left uncorrected: the server accepts signatures
|
|
4
|
+
* within ±30s, and the `Date` header only has whole-second resolution, so
|
|
5
|
+
* correcting a nearly-right clock would add noise without fixing anything.
|
|
6
|
+
*/
|
|
7
|
+
export const CLOCK_CORRECTION_THRESHOLD_MS = 5_000;
|
|
8
|
+
/** True when `Cache-Control` lets a cache store the response and serve it later. */
|
|
9
|
+
function mayBeCached(cacheControl) {
|
|
10
|
+
if (!cacheControl)
|
|
11
|
+
return false;
|
|
12
|
+
const directives = cacheControl.split(",").map((directive) => {
|
|
13
|
+
const [name = "", value = ""] = directive.split("=", 2);
|
|
14
|
+
return { name: name.trim().toLowerCase(), value: value.trim().replace(/^"(.*)"$/, "$1") };
|
|
15
|
+
});
|
|
16
|
+
if (directives.some(({ name }) => name === "no-store" || name === "no-cache"))
|
|
17
|
+
return false;
|
|
18
|
+
return directives.some(({ name, value }) => name === "immutable" || ((name === "max-age" || name === "s-maxage") && Number(value) > 0));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The server's notion of "now", learned from API response `Date` headers.
|
|
22
|
+
*
|
|
23
|
+
* Request signatures carry a timestamp the server rejects outside a ±30s
|
|
24
|
+
* window, so a device whose clock is minutes off fails every signed call.
|
|
25
|
+
* Each response reports the server's time; the offset from the local clock
|
|
26
|
+
* is applied to signing timestamps once it exceeds
|
|
27
|
+
* {@link CLOCK_CORRECTION_THRESHOLD_MS}.
|
|
28
|
+
*/
|
|
29
|
+
export class ServerClock {
|
|
30
|
+
offset = 0;
|
|
31
|
+
/** Milliseconds added to the local clock; 0 while the device clock is close enough. */
|
|
32
|
+
get offsetMs() {
|
|
33
|
+
return this.offset;
|
|
34
|
+
}
|
|
35
|
+
/** Current epoch milliseconds on the server's clock. */
|
|
36
|
+
now() {
|
|
37
|
+
return Date.now() + this.offset;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Record the server time from a response, skipping any the browser may
|
|
41
|
+
* have served from its HTTP cache: a cached copy keeps its original `Date`,
|
|
42
|
+
* which can be older than the ±30s signing window.
|
|
43
|
+
*/
|
|
44
|
+
observeResponse(headers, receivedAtMs) {
|
|
45
|
+
if (!headers || mayBeCached(headers.get("cache-control")))
|
|
46
|
+
return;
|
|
47
|
+
this.observe(headers.get("date"), receivedAtMs);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Record the server time from a response `Date` header.
|
|
51
|
+
*
|
|
52
|
+
* The server stamps `Date` when it sends the response, so it is compared
|
|
53
|
+
* with the local receipt time; request upload and server processing do not
|
|
54
|
+
* skew the estimate. The header is truncated to the second, so it is read
|
|
55
|
+
* as the middle of that second. A missing or malformed header leaves the
|
|
56
|
+
* offset unchanged.
|
|
57
|
+
*/
|
|
58
|
+
observe(dateHeader, receivedAtMs) {
|
|
59
|
+
if (!dateHeader)
|
|
60
|
+
return;
|
|
61
|
+
const serverMs = parseHttpDate(dateHeader.trim());
|
|
62
|
+
if (serverMs === undefined)
|
|
63
|
+
return;
|
|
64
|
+
const estimate = Math.round(serverMs + 500 - receivedAtMs);
|
|
65
|
+
this.offset = Math.abs(estimate) < CLOCK_CORRECTION_THRESHOLD_MS ? 0 : estimate;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xmonaco/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.67",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"homepage": "https://docs.0xmonaco.com/sdk/typescript",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"viem": "^2.45.2"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@0xmonaco/contracts": "1.0.
|
|
23
|
-
"@0xmonaco/types": "1.0.
|
|
22
|
+
"@0xmonaco/contracts": "1.0.67",
|
|
23
|
+
"@0xmonaco/types": "1.0.67",
|
|
24
24
|
"@noble/curves": "^1.9.1",
|
|
25
25
|
"@noble/hashes": "^2.4.0",
|
|
26
26
|
"http-status-codes": "^2.3.0"
|