@0xmonaco/core 1.0.66 → 1.0.69
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/buildercodes/api.d.ts +7 -2
- package/dist/api/buildercodes/api.js +10 -2
- package/dist/api/faucet/api.d.ts +2 -2
- package/dist/api/faucet/api.js +2 -2
- 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 +37 -6
- package/dist/api/whitelist/api.d.ts +4 -18
- package/dist/api/whitelist/api.js +4 -18
- 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);
|
|
@@ -31,7 +31,10 @@ export declare class BuildercodesAPIImpl extends BaseAPI implements Buildercodes
|
|
|
31
31
|
*/
|
|
32
32
|
getConfig(): Promise<GetBuildercodesConfigResponse>;
|
|
33
33
|
/**
|
|
34
|
-
* List this frontend's per-trade BuilderCodes credits, newest first.
|
|
34
|
+
* List this frontend's per-trade BuilderCodes credits, newest first. Each
|
|
35
|
+
* credit carries its payout `kind`: `REVENUE_SHARE` (your share of Monaco's
|
|
36
|
+
* fee) or `BUILDER_FEE` (your builder fee). Pass `kind` to list one stream;
|
|
37
|
+
* `total` then counts only that stream.
|
|
35
38
|
*
|
|
36
39
|
* The server scopes every query to the authenticated application; neither
|
|
37
40
|
* the application id nor payout wallet can be supplied by the caller.
|
|
@@ -40,7 +43,9 @@ export declare class BuildercodesAPIImpl extends BaseAPI implements Buildercodes
|
|
|
40
43
|
/**
|
|
41
44
|
* Summarize this frontend's BuilderCodes earnings by token.
|
|
42
45
|
*
|
|
43
|
-
* Amounts are RAW atomic units and remain separated by token.
|
|
46
|
+
* Amounts are RAW atomic units and remain separated by token. `byToken` sums
|
|
47
|
+
* every payout kind; `byTokenAndKind` splits the same totals into the
|
|
48
|
+
* revenue share and the builder fee.
|
|
44
49
|
*/
|
|
45
50
|
getPayoutSummary(): Promise<GetBuildercodePayoutSummaryResponse>;
|
|
46
51
|
/**
|
|
@@ -103,7 +103,10 @@ export class BuildercodesAPIImpl extends BaseAPI {
|
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
* List this frontend's per-trade BuilderCodes credits, newest first.
|
|
106
|
+
* List this frontend's per-trade BuilderCodes credits, newest first. Each
|
|
107
|
+
* credit carries its payout `kind`: `REVENUE_SHARE` (your share of Monaco's
|
|
108
|
+
* fee) or `BUILDER_FEE` (your builder fee). Pass `kind` to list one stream;
|
|
109
|
+
* `total` then counts only that stream.
|
|
107
110
|
*
|
|
108
111
|
* The server scopes every query to the authenticated application; neither
|
|
109
112
|
* the application id nor payout wallet can be supplied by the caller.
|
|
@@ -119,6 +122,9 @@ export class BuildercodesAPIImpl extends BaseAPI {
|
|
|
119
122
|
if (params?.pageSize !== undefined) {
|
|
120
123
|
searchParams.append("pageSize", params.pageSize.toString());
|
|
121
124
|
}
|
|
125
|
+
if (params?.kind !== undefined) {
|
|
126
|
+
searchParams.append("kind", params.kind);
|
|
127
|
+
}
|
|
122
128
|
const query = searchParams.toString();
|
|
123
129
|
const path = query ? `/api/v1/buildercodes/payouts?${query}` : "/api/v1/buildercodes/payouts";
|
|
124
130
|
return this.makeAuthenticatedRequest(path, { method: "GET" });
|
|
@@ -126,7 +132,9 @@ export class BuildercodesAPIImpl extends BaseAPI {
|
|
|
126
132
|
/**
|
|
127
133
|
* Summarize this frontend's BuilderCodes earnings by token.
|
|
128
134
|
*
|
|
129
|
-
* Amounts are RAW atomic units and remain separated by token.
|
|
135
|
+
* Amounts are RAW atomic units and remain separated by token. `byToken` sums
|
|
136
|
+
* every payout kind; `byTokenAndKind` splits the same totals into the
|
|
137
|
+
* revenue share and the builder fee.
|
|
130
138
|
*/
|
|
131
139
|
async getPayoutSummary() {
|
|
132
140
|
return this.makeAuthenticatedRequest("/api/v1/buildercodes/payouts/summary", { method: "GET" });
|
package/dist/api/faucet/api.d.ts
CHANGED
|
@@ -35,8 +35,8 @@ export declare class FaucetAPIImpl extends BaseAPI implements FaucetAPI {
|
|
|
35
35
|
*
|
|
36
36
|
* Read-only: remaining requests in the rolling 24h window, when the next
|
|
37
37
|
* request becomes available (`nextAvailableAt`, absent when nothing is in
|
|
38
|
-
* the window), and the 5 most recent requests.
|
|
39
|
-
* matching mint enforcement.
|
|
38
|
+
* the window), and the 5 most recent requests. Scoped to the wallet on this
|
|
39
|
+
* app (each app has its own quota), matching mint enforcement.
|
|
40
40
|
*
|
|
41
41
|
* @returns Promise resolving to the quota and recent-request history
|
|
42
42
|
*/
|
package/dist/api/faucet/api.js
CHANGED
|
@@ -40,8 +40,8 @@ export class FaucetAPIImpl extends BaseAPI {
|
|
|
40
40
|
*
|
|
41
41
|
* Read-only: remaining requests in the rolling 24h window, when the next
|
|
42
42
|
* request becomes available (`nextAvailableAt`, absent when nothing is in
|
|
43
|
-
* the window), and the 5 most recent requests.
|
|
44
|
-
* matching mint enforcement.
|
|
43
|
+
* the window), and the 5 most recent requests. Scoped to the wallet on this
|
|
44
|
+
* app (each app has its own quota), matching mint enforcement.
|
|
45
45
|
*
|
|
46
46
|
* @returns Promise resolving to the quota and recent-request history
|
|
47
47
|
*/
|
|
@@ -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,10 +174,14 @@ 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
|
-
// Populated on the subscribe-time snapshot frame
|
|
170
|
-
//
|
|
171
|
-
// value from being silently dropped before the subscriber.
|
|
183
|
+
// Populated on live updates and on the subscribe-time snapshot frame
|
|
184
|
+
// (0XM-2440); absent only for a row written before the counter existed.
|
|
172
185
|
version: readOptionalNumber(data, "version"),
|
|
173
186
|
};
|
|
174
187
|
}
|
|
@@ -583,6 +596,7 @@ function parseOrderSnapshotItem(raw) {
|
|
|
583
596
|
// `clientOrderId: undefined`, which a structural comparison would see.
|
|
584
597
|
const clientOrderId = readOptionalString(item, "clientOrderId");
|
|
585
598
|
const selfTradePreventionMode = readOptionalString(item, "selfTradePreventionMode");
|
|
599
|
+
const parentOrderId = readOptionalString(item, "parentOrderId");
|
|
586
600
|
const version = readOptionalNumber(item, "version");
|
|
587
601
|
return {
|
|
588
602
|
orderId: readString(item, "orderId"),
|
|
@@ -617,6 +631,9 @@ function parseOrderSnapshotItem(raw) {
|
|
|
617
631
|
// Spread in conditionally like `clientOrderId`: absent means the order
|
|
618
632
|
// requested no override, and must stay absent (0XM-2733).
|
|
619
633
|
...(selfTradePreventionMode === undefined ? {} : { selfTradePreventionMode }),
|
|
634
|
+
// Spread in conditionally too: absent for an order that is not a TWAP
|
|
635
|
+
// child, and absent must stay absent.
|
|
636
|
+
...(parentOrderId === undefined ? {} : { parentOrderId }),
|
|
620
637
|
// Spread in conditionally for the same reason as `clientOrderId`: the
|
|
621
638
|
// server omits the key when unknown, and absent must stay absent rather
|
|
622
639
|
// than becoming `version: undefined` — a resync compares this value, and
|
|
@@ -669,8 +686,12 @@ function snapshotFrameHandler(channel, parseItem, onSnapshot) {
|
|
|
669
686
|
}
|
|
670
687
|
/**
|
|
671
688
|
* Create a Monaco WebSocket client
|
|
689
|
+
*
|
|
690
|
+
* @param serverClock - Server time used to stamp the auth handshake; the SDK
|
|
691
|
+
* passes the clock its REST clients calibrate so a skewed device clock does
|
|
692
|
+
* not fail authentication.
|
|
672
693
|
*/
|
|
673
|
-
export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
694
|
+
export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new ServerClock()) {
|
|
674
695
|
let ws = null;
|
|
675
696
|
let session = options.session;
|
|
676
697
|
let reconnectAttempts = 0;
|
|
@@ -698,6 +719,8 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
698
719
|
// connection never decides how a later connection's close is read.
|
|
699
720
|
let lastAuthError = null;
|
|
700
721
|
let pendingAuthSessions = [];
|
|
722
|
+
// Consecutive handshakes rejected for clock skew; reset on success.
|
|
723
|
+
let skewAuthRetries = 0;
|
|
701
724
|
let confirmedSession;
|
|
702
725
|
// Handler storage: channel -> set of handlers (supports multiple subscribers per channel)
|
|
703
726
|
const handlers = new Map();
|
|
@@ -746,7 +769,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
746
769
|
const sendAuthenticate = () => {
|
|
747
770
|
if (!session || ws?.readyState !== WebSocket.OPEN)
|
|
748
771
|
return;
|
|
749
|
-
const timestamp =
|
|
772
|
+
const timestamp = serverClock.now();
|
|
750
773
|
const signature = signMessage(hexToBytes(session.privateKey), composeWsSigningString(session.publicKey, timestamp));
|
|
751
774
|
pendingAuthSessions.push(session);
|
|
752
775
|
ws.send(JSON.stringify({
|
|
@@ -881,6 +904,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
881
904
|
}
|
|
882
905
|
if (msg.type === "Authenticated") {
|
|
883
906
|
confirmedSession = pendingAuthSessions.shift();
|
|
907
|
+
skewAuthRetries = 0;
|
|
884
908
|
return;
|
|
885
909
|
}
|
|
886
910
|
if (!msg.channel)
|
|
@@ -1000,7 +1024,13 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1000
1024
|
// (e.g. re-authenticating from onError): the fresh session is what
|
|
1001
1025
|
// the reconnect uses.
|
|
1002
1026
|
const authError = lastAuthError;
|
|
1003
|
-
|
|
1027
|
+
// A handshake stamped outside the server's window is a clock
|
|
1028
|
+
// problem, not a dead session: reconnect and re-sign, a bounded
|
|
1029
|
+
// number of times, instead of discarding the credentials.
|
|
1030
|
+
const skewRetry = autoReconnect && authError?.code === "AUTH_FAILED" && SKEW_REJECTION.test(authError.message) && skewAuthRetries < MAX_SKEW_AUTH_RETRIES;
|
|
1031
|
+
if (skewRetry)
|
|
1032
|
+
skewAuthRetries++;
|
|
1033
|
+
const authLoss = !skewRetry && authError !== null && authError.session !== undefined && (authError.code === "AUTH_FAILED" || event.code === 1008);
|
|
1004
1034
|
const terminalAuthLoss = authLoss && session === authError.session;
|
|
1005
1035
|
if (terminalAuthLoss) {
|
|
1006
1036
|
session = undefined;
|
|
@@ -1665,6 +1695,7 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
|
|
|
1665
1695
|
getStatus,
|
|
1666
1696
|
setSessionKeypair: (credentials) => {
|
|
1667
1697
|
session = credentials ?? undefined;
|
|
1698
|
+
skewAuthRetries = 0;
|
|
1668
1699
|
// If already connected, (re)send the signed handshake so the connection
|
|
1669
1700
|
// picks up the new session without tearing down. If not yet connected,
|
|
1670
1701
|
// onopen will send it. Clearing the session leaves the connection up for
|
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Public (unauthenticated) whitelist/waitlist application submission. The
|
|
5
|
-
* server validates and de-duplicates by wallet address and email, creating an
|
|
6
|
-
* inactive user pending manual approval. This is an onboarding endpoint, not a
|
|
7
|
-
* trading operation.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* const { message, userId } = await sdk.whitelist.submit({
|
|
12
|
-
* walletAddress: "0x...",
|
|
13
|
-
* email: "user@example.com",
|
|
14
|
-
* });
|
|
15
|
-
* ```
|
|
2
|
+
* Retained client for the retired public whitelist endpoint.
|
|
3
|
+
* Submissions return HTTP 403 FORBIDDEN without changing applicant data.
|
|
16
4
|
*/
|
|
17
5
|
import type { SubmitWhitelistRequest, SubmitWhitelistResponse, WhitelistAPI } from "@0xmonaco/types";
|
|
18
6
|
import { BaseAPI } from "../base";
|
|
19
7
|
export declare class WhitelistAPIImpl extends BaseAPI implements WhitelistAPI {
|
|
20
8
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param body - Applicant details
|
|
24
|
-
* @returns Promise resolving to the status message and created user id
|
|
9
|
+
* @deprecated Public whitelist applications are closed. The server returns HTTP 403.
|
|
10
|
+
* @param body - Legacy applicant details; the request shape is retained for compatibility.
|
|
25
11
|
*/
|
|
26
12
|
submit(body: SubmitWhitelistRequest): Promise<SubmitWhitelistResponse>;
|
|
27
13
|
}
|
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Public (unauthenticated) whitelist/waitlist application submission. The
|
|
5
|
-
* server validates and de-duplicates by wallet address and email, creating an
|
|
6
|
-
* inactive user pending manual approval. This is an onboarding endpoint, not a
|
|
7
|
-
* trading operation.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* const { message, userId } = await sdk.whitelist.submit({
|
|
12
|
-
* walletAddress: "0x...",
|
|
13
|
-
* email: "user@example.com",
|
|
14
|
-
* });
|
|
15
|
-
* ```
|
|
2
|
+
* Retained client for the retired public whitelist endpoint.
|
|
3
|
+
* Submissions return HTTP 403 FORBIDDEN without changing applicant data.
|
|
16
4
|
*/
|
|
17
5
|
import { BaseAPI } from "../base";
|
|
18
6
|
import { perpRoutes } from "../perp/routes";
|
|
19
7
|
export class WhitelistAPIImpl extends BaseAPI {
|
|
20
8
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param body - Applicant details
|
|
24
|
-
* @returns Promise resolving to the status message and created user id
|
|
9
|
+
* @deprecated Public whitelist applications are closed. The server returns HTTP 403.
|
|
10
|
+
* @param body - Legacy applicant details; the request shape is retained for compatibility.
|
|
25
11
|
*/
|
|
26
12
|
async submit(body) {
|
|
27
13
|
return await this.makePublicRequest(perpRoutes.whitelist.submit(), {
|
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.69",
|
|
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.69",
|
|
23
|
+
"@0xmonaco/types": "1.0.69",
|
|
24
24
|
"@noble/curves": "^1.9.1",
|
|
25
25
|
"@noble/hashes": "^2.4.0",
|
|
26
26
|
"http-status-codes": "^2.3.0"
|