@0xmonaco/core 0.8.5 → 0.8.7-develop.0f12336

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.
Files changed (57) hide show
  1. package/README.md +3 -12
  2. package/dist/api/applications/api.d.ts +61 -8
  3. package/dist/api/applications/api.js +75 -11
  4. package/dist/api/auth/api.d.ts +44 -76
  5. package/dist/api/auth/api.js +68 -111
  6. package/dist/api/base.d.ts +50 -9
  7. package/dist/api/base.js +97 -14
  8. package/dist/api/delegated-agents/api.d.ts +2 -1
  9. package/dist/api/delegated-agents/api.js +15 -11
  10. package/dist/api/faucet/api.d.ts +25 -0
  11. package/dist/api/faucet/api.js +29 -0
  12. package/dist/api/faucet/index.d.ts +1 -0
  13. package/dist/api/faucet/index.js +1 -0
  14. package/dist/api/fees/api.d.ts +13 -13
  15. package/dist/api/fees/api.js +18 -18
  16. package/dist/api/index.d.ts +4 -0
  17. package/dist/api/index.js +4 -0
  18. package/dist/api/margin-accounts/api.d.ts +9 -5
  19. package/dist/api/margin-accounts/api.js +70 -33
  20. package/dist/api/market/api.d.ts +5 -2
  21. package/dist/api/market/api.js +32 -16
  22. package/dist/api/orderbook/api.js +13 -12
  23. package/dist/api/perp/routes.d.ts +113 -37
  24. package/dist/api/perp/routes.js +37 -6
  25. package/dist/api/positions/api.d.ts +2 -1
  26. package/dist/api/positions/api.js +30 -18
  27. package/dist/api/profile/api.d.ts +40 -23
  28. package/dist/api/profile/api.js +78 -38
  29. package/dist/api/sub-accounts/api.d.ts +62 -0
  30. package/dist/api/sub-accounts/api.js +80 -0
  31. package/dist/api/sub-accounts/index.d.ts +1 -0
  32. package/dist/api/sub-accounts/index.js +1 -0
  33. package/dist/api/trades/api.d.ts +21 -10
  34. package/dist/api/trades/api.js +22 -10
  35. package/dist/api/trading/api.d.ts +11 -8
  36. package/dist/api/trading/api.js +76 -90
  37. package/dist/api/vault/api.d.ts +96 -29
  38. package/dist/api/vault/api.js +172 -43
  39. package/dist/api/vault/index.d.ts +1 -1
  40. package/dist/api/vault/index.js +1 -1
  41. package/dist/api/websocket/types.d.ts +5 -5
  42. package/dist/api/websocket/websocket.js +43 -22
  43. package/dist/api/whitelist/api.d.ts +27 -0
  44. package/dist/api/whitelist/api.js +32 -0
  45. package/dist/api/whitelist/index.d.ts +1 -0
  46. package/dist/api/whitelist/index.js +1 -0
  47. package/dist/api/withdrawals/api.d.ts +18 -0
  48. package/dist/api/withdrawals/api.js +35 -0
  49. package/dist/api/withdrawals/index.d.ts +1 -0
  50. package/dist/api/withdrawals/index.js +1 -0
  51. package/dist/coverage.d.ts +93 -0
  52. package/dist/coverage.js +93 -0
  53. package/dist/crypto/session.d.ts +40 -0
  54. package/dist/crypto/session.js +60 -0
  55. package/dist/sdk.d.ts +58 -20
  56. package/dist/sdk.js +156 -53
  57. package/package.json +5 -3
@@ -28,6 +28,31 @@ import { ApproveTokenSchema, DepositSchema, validate, WithdrawSchema } from "@0x
28
28
  import { erc20Abi, zeroAddress } from "viem";
29
29
  import { APIError, ContractError, InvalidConfigError } from "../../errors";
30
30
  import { BaseAPI } from "../base";
31
+ /** Default delay between polls while waiting for a withdrawal's proof. */
32
+ const DEFAULT_WITHDRAWAL_POLL_INTERVAL_MS = 5_000;
33
+ /**
34
+ * Default total wait for a withdrawal's proof (5 minutes). A withdrawal's merkle
35
+ * proof only lands after its root is confirmed on-chain, which on a healthy
36
+ * pipeline takes ~20s but can lag behind a brief proposer hiccup; the previous
37
+ * 60s tripped too eagerly. On timeout the SDK returns `awaiting_proof` rather
38
+ * than throwing, so the caller never loses the index — it just retries later.
39
+ */
40
+ const DEFAULT_WITHDRAWAL_TIMEOUT_MS = 300_000;
41
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42
+ /**
43
+ * Encode the on-chain `applicationData` for a deposit.
44
+ *
45
+ * For `"spot"` (the default) this stays the bare `clientId` string — byte-for-byte
46
+ * identical to the legacy encoding, so existing deposits are unchanged. For
47
+ * `"margin"` it emits a small JSON routing hint the indexer decodes to credit the
48
+ * deposit to margin collateral.
49
+ *
50
+ * MUST stay in sync with the indexer decoder in
51
+ * `indexer/src/listeners/deposit_routing.rs`.
52
+ */
53
+ export function encodeDepositApplicationData(clientId, target) {
54
+ return target === "margin" ? JSON.stringify({ clientId, depositTarget: "MARGIN" }) : clientId;
55
+ }
31
56
  export class VaultAPIImpl extends BaseAPI {
32
57
  publicClient;
33
58
  chain;
@@ -196,6 +221,10 @@ export class VaultAPIImpl extends BaseAPI {
196
221
  * @param assetId - The asset identifier (UUID) to deposit
197
222
  * @param amount - The amount of tokens to deposit (as bigint)
198
223
  * @param autoWait - Whether to automatically wait for transaction confirmation (defaults to true)
224
+ * @param target - Destination ledger: `"spot"` (default) credits the spot
225
+ * wallet; `"margin"` routes the deposit into the parent margin account's
226
+ * collateral (auto-creating the account if needed). Margin deposits that
227
+ * cannot be routed safely fall back to spot.
199
228
  * @returns Promise resolving to TransactionResult with transaction details
200
229
  * @throws {ContractError} When deposit fails or approval is insufficient
201
230
  * @throws {APIError} When the asset is not found or the assetId is invalid
@@ -211,6 +240,14 @@ export class VaultAPIImpl extends BaseAPI {
211
240
  * console.log(`Deposit transaction: ${result.hash}`);
212
241
  * console.log(`Status: ${result.status}`); // "confirmed" if successful
213
242
  *
243
+ * // Deposit straight into margin collateral
244
+ * const marginDeposit = await vaultAPI.deposit(
245
+ * "123e4567-e89b-12d3-a456-426614174000",
246
+ * parseUnits("100", 6),
247
+ * true,
248
+ * "margin"
249
+ * );
250
+ *
214
251
  * // Or skip auto-waiting
215
252
  * const result = await vaultAPI.deposit(
216
253
  * "123e4567-e89b-12d3-a456-426614174000",
@@ -221,14 +258,17 @@ export class VaultAPIImpl extends BaseAPI {
221
258
  * const receipt = await sdk.waitForTransaction(result.hash);
222
259
  * ```
223
260
  */
224
- async deposit(assetId, amount, autoWait = true) {
261
+ async deposit(assetId, amount, autoWait = true, target = "spot") {
225
262
  if (!this.walletClient) {
226
263
  throw new InvalidConfigError("Wallet client not set. Connect a wallet first.", "walletClient");
227
264
  }
228
265
  // Validate inputs
229
- validate(DepositSchema, { assetId, amount, autoWait });
266
+ validate(DepositSchema, { assetId, amount, autoWait, target });
230
267
  const vaultAddress = await this.getVaultAddress();
231
268
  const clientId = await this.getClientId();
269
+ // The on-chain applicationData carries the client id plus, for margin
270
+ // deposits, the routing hint the indexer decodes.
271
+ const applicationData = encodeDepositApplicationData(clientId, target);
232
272
  const { tokenAddress, isNativeToken } = await this.resolveAsset(assetId);
233
273
  if (!isNativeToken) {
234
274
  // Check if approval is needed before proceeding
@@ -250,7 +290,7 @@ export class VaultAPIImpl extends BaseAPI {
250
290
  address: vaultAddress,
251
291
  abi: CONTRACT_ABIS.vault,
252
292
  functionName: "depositNative",
253
- args: [walletAccount.address, clientId],
293
+ args: [walletAccount.address, applicationData],
254
294
  account: walletAccount,
255
295
  chain: this.chain,
256
296
  value: amount,
@@ -261,7 +301,7 @@ export class VaultAPIImpl extends BaseAPI {
261
301
  address: vaultAddress,
262
302
  abi: CONTRACT_ABIS.vault,
263
303
  functionName: "depositERC20",
264
- args: [walletAccount.address, clientId, tokenAddress, amount],
304
+ args: [walletAccount.address, applicationData, tokenAddress, amount],
265
305
  account: walletAccount,
266
306
  chain: this.chain,
267
307
  });
@@ -275,21 +315,80 @@ export class VaultAPIImpl extends BaseAPI {
275
315
  return await this.waitForTransaction(txResult, autoWait);
276
316
  }
277
317
  /**
278
- * Initiates a withdrawal through the API Gateway and submits the resulting
279
- * pre-signed calldata on-chain via the connected wallet.
318
+ * Polls `GET /api/v1/withdrawals/{index}` until the gateway can serve the
319
+ * `executeWithdrawal` calldata, then returns it with the vault address.
320
+ *
321
+ * The calldata only exists once the withdrawal's root is confirmed on-chain
322
+ * and its merkle proof is persisted. Until then the gateway returns 404 (the
323
+ * row is written asynchronously by the persistor after `initiate`) or 409
324
+ * (the proof is not confirmed yet); both are transient for a freshly-allocated
325
+ * index, so we retry on each until ready or the timeout elapses.
280
326
  *
281
- * The gateway allocates a `withdrawalIndex` via the matching engine and returns
282
- * ABI-encoded calldata for `executeSignedWithdrawal(...)` signed by the
283
- * server-side `WITHDRAWAL_SIGNER`. The user's wallet pays gas to submit it,
284
- * but the contract authenticates against the embedded signature — not the
285
- * `msg.sender`. The connected wallet's address is sent as the on-chain
286
- * `destination`.
327
+ * Returns `null` when the timeout elapses before the proof is available — the
328
+ * index is still valid and the balance already debited, so callers surface
329
+ * this as `awaiting_proof` (with the index intact) rather than as a thrown
330
+ * error that would strand the index. Only non-transient errors (anything
331
+ * other than 404/409) throw.
332
+ *
333
+ * @throws {APIError} On non-transient errors while polling.
334
+ */
335
+ async pollWithdrawalCalldata(withdrawalIndex, retry) {
336
+ const pollIntervalMs = retry?.pollIntervalMs ?? DEFAULT_WITHDRAWAL_POLL_INTERVAL_MS;
337
+ const timeoutMs = retry?.timeoutMs ?? DEFAULT_WITHDRAWAL_TIMEOUT_MS;
338
+ const deadline = Date.now() + timeoutMs;
339
+ for (;;) {
340
+ try {
341
+ const { vaultAddress, calldata } = await this.makePublicRequest(`/api/v1/withdrawals/${withdrawalIndex}`);
342
+ // Ready: non-empty calldata. (Defensively treat an empty/`0x` body as
343
+ // not-ready too, though the gateway returns an error in that case.)
344
+ if (calldata && calldata !== "0x") {
345
+ return { vaultAddress, calldata };
346
+ }
347
+ }
348
+ catch (error) {
349
+ // 404 (row not persisted yet) and 409 (proof not confirmed yet) are
350
+ // transient for a valid index — keep polling. Anything else is fatal.
351
+ const isTransient = error instanceof APIError && (error.statusCode === 404 || error.statusCode === 409);
352
+ if (!isTransient) {
353
+ throw error;
354
+ }
355
+ }
356
+ if (Date.now() + pollIntervalMs > deadline) {
357
+ // Timed out waiting for the proof. Signal not-ready; the caller keeps the
358
+ // index and retries later rather than losing it inside a thrown error.
359
+ return null;
360
+ }
361
+ await sleep(pollIntervalMs);
362
+ }
363
+ }
364
+ /**
365
+ * Initiates a withdrawal through the API Gateway, waits for its on-chain
366
+ * confirmation, then submits the resulting `executeWithdrawal` calldata via
367
+ * the connected wallet.
368
+ *
369
+ * The gateway allocates a `withdrawalIndex` via the matching engine and debits
370
+ * the balance immediately, but the executable calldata needs the withdrawal's
371
+ * merkle proof, which only exists once its root is confirmed on-chain. This
372
+ * method polls until the calldata is available (see `retry`), then submits it.
373
+ * The connected wallet pays gas; the contract authorises the withdrawal
374
+ * against the merkle proof embedded in the calldata. The wallet's address is
375
+ * sent as the on-chain `destination`.
376
+ *
377
+ * If the proof does not become available within the poll window, this method
378
+ * does NOT throw — it returns `{ withdrawalIndex, status: "awaiting_proof" }`
379
+ * with nothing submitted on-chain. The balance is already debited, so losing
380
+ * the index would strand the funds; the caller keeps it and finishes the
381
+ * withdrawal later with `retryWithdrawal(withdrawalIndex)`.
287
382
  *
288
383
  * @param assetId - The asset identifier (UUID) to withdraw
289
384
  * @param amount - The raw token amount to withdraw (as bigint)
290
385
  * @param autoWait - Whether to automatically wait for transaction confirmation (defaults to true)
291
- * @returns Promise resolving to `{ withdrawalIndex, transaction }`
292
- * @throws {APIError} When the asset is not found or the request is rejected
386
+ * @param sourceOrRetry - Source ledger (`"spot"` or `"margin"`) or legacy retry options
387
+ * @param retry - Polling cadence/timeout while waiting for the proof when a source is supplied
388
+ * @returns Promise resolving to a [`WithdrawResult`]: on success the submitted
389
+ * transaction plus `withdrawalIndex`; on poll-timeout `{ withdrawalIndex,
390
+ * status: "awaiting_proof" }`.
391
+ * @throws {APIError} When the asset is not found or the initiate request is rejected
293
392
  * @throws {ContractError} When the on-chain submission fails
294
393
  * @throws {InvalidConfigError} When wallet account is not available
295
394
  *
@@ -301,18 +400,23 @@ export class VaultAPIImpl extends BaseAPI {
301
400
  * parseUnits("50", 6),
302
401
  * );
303
402
  * console.log(`Withdrawal index: ${result.withdrawalIndex}`);
304
- * console.log(`Tx hash: ${result.hash}, status: ${result.status}`);
403
+ * if (result.status === "awaiting_proof") {
404
+ * // Proof not ready yet — balance already debited. Finish later:
405
+ * await vaultAPI.retryWithdrawal(result.withdrawalIndex);
406
+ * } else {
407
+ * console.log(`Tx hash: ${result.hash}, status: ${result.status}`);
408
+ * }
305
409
  *
306
- * // Or skip auto-waiting and finalise later
410
+ * // Custom polling (check every 10s, wait up to an hour)
307
411
  * const result = await vaultAPI.withdraw(
308
412
  * "123e4567-e89b-12d3-a456-426614174000",
309
413
  * parseUnits("50", 6),
310
- * false,
414
+ * true,
415
+ * { pollIntervalMs: 10_000, timeoutMs: 3_600_000 },
311
416
  * );
312
- * const receipt = await sdk.waitForTransaction(result.hash);
313
417
  * ```
314
418
  */
315
- async withdraw(assetId, amount, autoWait = true) {
419
+ async withdraw(assetId, amount, autoWait = true, sourceOrRetry, retry) {
316
420
  if (!this.walletClient) {
317
421
  throw new InvalidConfigError("Wallet client not set. Connect a wallet first.", "walletClient");
318
422
  }
@@ -321,21 +425,33 @@ export class VaultAPIImpl extends BaseAPI {
321
425
  throw new InvalidConfigError("No account available in wallet client", "account");
322
426
  }
323
427
  const destination = walletAccount.address.toLowerCase();
324
- validate(WithdrawSchema, { assetId, amount, destination, autoWait });
325
- // 1. Gateway allocates withdrawal_index + returns pre-signed
326
- // executeSignedWithdrawal calldata along with the vault address to send
327
- // it to. Sourcing the vault address from this response (instead of
328
- // GET /applications/config) keeps the calldata + target contract in
329
- // lockstep and avoids an extra round-trip.
330
- const { withdrawal_index: withdrawalIndex, vault_address: vaultAddress, calldata, } = await this.makeAuthenticatedRequest("/api/v1/withdrawals", {
428
+ const source = typeof sourceOrRetry === "string" ? sourceOrRetry : "spot";
429
+ const retryOptions = typeof sourceOrRetry === "string" ? retry : sourceOrRetry;
430
+ validate(WithdrawSchema, { assetId, amount, destination, autoWait, source });
431
+ // 1. Gateway allocates the withdrawalIndex and debits the balance. No
432
+ // executable calldata yet — it needs the merkle proof, available only once
433
+ // the withdrawal root is confirmed on-chain.
434
+ const { withdrawalIndex } = await this.makeAuthenticatedRequest("/api/v1/withdrawals", {
331
435
  method: "POST",
332
436
  body: JSON.stringify({
333
- asset_id: assetId,
437
+ assetId: assetId,
334
438
  amount: amount.toString(),
335
439
  destination,
440
+ source,
336
441
  }),
337
442
  });
338
- // 2. Submit the calldata on-chain through the connected wallet.
443
+ // 2. Poll for the executeWithdrawal calldata until the proof is confirmed.
444
+ // The vault address comes back with it, keeping calldata + target contract
445
+ // in lockstep.
446
+ const poll = await this.pollWithdrawalCalldata(withdrawalIndex, retryOptions);
447
+ if (!poll) {
448
+ // Proof not available within the poll window. The balance is already
449
+ // debited and the index is valid; return it (without submitting anything
450
+ // on-chain) so the caller can finish later via retryWithdrawal(index).
451
+ return { withdrawalIndex, status: "awaiting_proof" };
452
+ }
453
+ const { vaultAddress, calldata } = poll;
454
+ // 3. Submit the calldata on-chain through the connected wallet.
339
455
  let hash;
340
456
  try {
341
457
  hash = await this.walletClient.sendTransaction({
@@ -346,7 +462,7 @@ export class VaultAPIImpl extends BaseAPI {
346
462
  });
347
463
  }
348
464
  catch (error) {
349
- throw new ContractError(`Failed to submit executeSignedWithdrawal for withdrawal ${withdrawalIndex}: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error instanceof Error ? error : undefined });
465
+ throw new ContractError(`Failed to submit executeWithdrawal for withdrawal ${withdrawalIndex}: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error instanceof Error ? error : undefined });
350
466
  }
351
467
  const nonce = walletAccount.getNonce ? await walletAccount.getNonce() : 0n;
352
468
  const txResult = {
@@ -358,25 +474,32 @@ export class VaultAPIImpl extends BaseAPI {
358
474
  return { ...settled, withdrawalIndex };
359
475
  }
360
476
  /**
361
- * Retries a previously-initiated withdrawal whose on-chain submission never
362
- * landed — e.g. the wallet rejected the tx, the page reloaded before the
363
- * receipt came back, or a stuck mempool entry needs resending.
477
+ * Submits (or resubmits) a previously-initiated withdrawal on-chain.
478
+ *
479
+ * Use this when the original `withdraw()` could not complete — e.g. the proof
480
+ * was not yet confirmed within its polling window, the wallet rejected the tx,
481
+ * the page reloaded before the receipt came back, or a stuck mempool entry
482
+ * needs resending. Polls `GET /withdrawals/{index}` for the
483
+ * `executeWithdrawal` calldata (retrying while it is not yet confirmed), then
484
+ * submits it through the connected wallet. Does NOT initiate a new withdrawal
485
+ * — the matching engine already debited the balance and allocated the index.
486
+ * The contract is idempotent against double-submission of a settled
487
+ * withdrawal: it reverts once the index is consumed on-chain.
364
488
  *
365
- * Re-fetches the same `executeSignedWithdrawal` calldata the gateway
366
- * generated when the withdrawal was initiated, then submits it through the
367
- * connected wallet. Does NOT initiate a new withdrawal — the matching engine
368
- * already debited the balance and allocated the index. The contract is
369
- * idempotent against double-submission of a settled withdrawal: it will
370
- * revert once the index is consumed on-chain.
489
+ * Like `withdraw()`, if the proof is still unavailable within the poll window
490
+ * this returns `{ withdrawalIndex, status: "awaiting_proof" }` instead of
491
+ * throwing, so the caller can simply retry again later.
371
492
  *
372
493
  * @param withdrawalIndex - The index returned by the original `withdraw()` call
373
494
  * @param autoWait - Whether to await on-chain confirmation (defaults to true)
374
- * @returns Promise resolving to `{ withdrawalIndex, ...transaction }`
375
- * @throws {APIError} When the index doesn't exist
495
+ * @param retry - Polling cadence/timeout while waiting for the proof
496
+ * @returns Promise resolving to a [`WithdrawResult`]: the submitted transaction
497
+ * plus `withdrawalIndex`, or `{ withdrawalIndex, status: "awaiting_proof" }`
498
+ * if the proof did not arrive within the poll window.
376
499
  * @throws {ContractError} When the on-chain submission fails
377
500
  * @throws {InvalidConfigError} When wallet account is not available
378
501
  */
379
- async retryWithdrawal(withdrawalIndex, autoWait = true) {
502
+ async retryWithdrawal(withdrawalIndex, autoWait = true, retry) {
380
503
  if (!this.walletClient) {
381
504
  throw new InvalidConfigError("Wallet client not set. Connect a wallet first.", "walletClient");
382
505
  }
@@ -384,7 +507,13 @@ export class VaultAPIImpl extends BaseAPI {
384
507
  if (!walletAccount) {
385
508
  throw new InvalidConfigError("No account available in wallet client", "account");
386
509
  }
387
- const { vault_address: vaultAddress, calldata } = await this.makePublicRequest(`/api/v1/withdrawals/${withdrawalIndex}`);
510
+ const poll = await this.pollWithdrawalCalldata(withdrawalIndex, retry);
511
+ if (!poll) {
512
+ // Still not confirmed on-chain within the poll window — return the index
513
+ // so the caller can retry again later, rather than throwing.
514
+ return { withdrawalIndex, status: "awaiting_proof" };
515
+ }
516
+ const { vaultAddress, calldata } = poll;
388
517
  let hash;
389
518
  try {
390
519
  hash = await this.walletClient.sendTransaction({
@@ -395,7 +524,7 @@ export class VaultAPIImpl extends BaseAPI {
395
524
  });
396
525
  }
397
526
  catch (error) {
398
- throw new ContractError(`Failed to resubmit executeSignedWithdrawal for withdrawal ${withdrawalIndex}: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error instanceof Error ? error : undefined });
527
+ throw new ContractError(`Failed to resubmit executeWithdrawal for withdrawal ${withdrawalIndex}: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error instanceof Error ? error : undefined });
399
528
  }
400
529
  const nonce = walletAccount.getNonce ? await walletAccount.getNonce() : 0n;
401
530
  const txResult = {
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Vault API Module
3
3
  */
4
- export { VaultAPIImpl } from "./api";
4
+ export { encodeDepositApplicationData, VaultAPIImpl } from "./api";
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Vault API Module
3
3
  */
4
- export { VaultAPIImpl } from "./api";
4
+ export { encodeDepositApplicationData, VaultAPIImpl } from "./api";
@@ -1,9 +1,9 @@
1
- import type { ConditionalOrderEvent, Interval, OHLCVEvent, OrderbookEvent, OrderbookQuotationMode, OrderEvent, TradeEvent, TradingMode, UserBalanceEvent, UserMovementEvent, WebSocketStatus } from "@0xmonaco/types";
1
+ import type { ConditionalOrderEvent, Interval, OHLCVEvent, OrderbookEvent, OrderbookQuotationMode, OrderEvent, SessionCredentials, TradeEvent, TradingMode, UserBalanceEvent, UserMovementEvent, WebSocketStatus } from "@0xmonaco/types";
2
2
  export type StatusHandler = (status: WebSocketStatus) => void;
3
3
  export type MessageHandler<T> = (data: T) => void;
4
4
  export interface MonacoWebSocketOptions {
5
- /** JWT access token for authenticated channels (orders) */
6
- token?: string;
5
+ /** Session keypair for authenticated channels (orders, balances, etc.) */
6
+ session?: SessionCredentials;
7
7
  /** Enable auto-reconnect on disconnect (default: true) */
8
8
  autoReconnect?: boolean;
9
9
  /** Maximum reconnection attempts (default: 5) */
@@ -20,8 +20,8 @@ export interface MonacoWebSocket {
20
20
  isConnected: () => boolean;
21
21
  /** Get current websocket connection status */
22
22
  getStatus: () => WebSocketStatus;
23
- /** Update the access token (for re-auth) */
24
- setToken: (token: string) => void;
23
+ /** Set (or clear) the session keypair used to authenticate the connection */
24
+ setSessionKeypair: (credentials: SessionCredentials | undefined) => void;
25
25
  /** Subscribe to order events (requires authentication) */
26
26
  orders: (tradingPairId: string, tradingMode: TradingMode, handler: MessageHandler<OrderEvent>) => () => void;
27
27
  /** Subscribe to orderbook events (public) */
@@ -1,3 +1,5 @@
1
+ import { hexToBytes } from "@noble/hashes/utils";
2
+ import { composeWsSigningString, signMessage } from "../../crypto/session";
1
3
  import { ALL_MAGNITUDES } from "../../utils";
2
4
  import { keysToCamelCase } from "./utils";
3
5
  // Connection constants
@@ -107,7 +109,7 @@ function parseConditionalOrderEvent(rawData) {
107
109
  */
108
110
  export function createMonacoWebSocket(baseUrl, options = {}) {
109
111
  let ws = null;
110
- let token = options.token;
112
+ let session = options.session;
111
113
  let reconnectAttempts = 0;
112
114
  let reconnectTimer = null;
113
115
  let heartbeatTimer = null;
@@ -127,11 +129,24 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
127
129
  return "disconnected";
128
130
  }
129
131
  };
130
- const getUrl = () => {
131
- const url = new URL(baseUrl);
132
- if (token)
133
- url.searchParams.set("token", token);
134
- return url.toString();
132
+ const getUrl = () => baseUrl;
133
+ /**
134
+ * Send the signed auth handshake as the first message after the socket
135
+ * opens. The client signs `WS-AUTH\n<pubkey>\n<ts>` with the session private
136
+ * key; the server binds the connection to the session. No-op if no session
137
+ * is set (public channels work unauthenticated).
138
+ */
139
+ const sendAuthenticate = () => {
140
+ if (!session || ws?.readyState !== WebSocket.OPEN)
141
+ return;
142
+ const timestamp = Date.now();
143
+ const signature = signMessage(hexToBytes(session.privateKey), composeWsSigningString(session.publicKey, timestamp));
144
+ ws.send(JSON.stringify({
145
+ type: "Authenticate",
146
+ public_key: session.publicKey,
147
+ timestamp,
148
+ signature,
149
+ }));
135
150
  };
136
151
  const stopHeartbeat = () => {
137
152
  if (heartbeatTimer) {
@@ -217,6 +232,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
217
232
  }
218
233
  reconnectAttempts = 0;
219
234
  startHeartbeat();
235
+ // Authenticate before resubscribing so user-specific channels
236
+ // (orders, balances, …) are authorized when the Subscribe arrives.
237
+ sendAuthenticate();
220
238
  resubscribeAll();
221
239
  options.onStatusChange?.("connected");
222
240
  resolve();
@@ -324,7 +342,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
324
342
  const data = rawData;
325
343
  const orderbookData = data.data;
326
344
  const event = {
327
- tradingPairId: data.symbol,
345
+ // `trading_pair_id` is the pair UUID; `symbol` is the display string
346
+ // (e.g. "AMZN/USDC"). `tradingPairId` must carry the UUID.
347
+ tradingPairId: data.trading_pair_id,
328
348
  tradingMode: data.trading_mode,
329
349
  bids: (orderbookData?.bids || []).map((level) => ({
330
350
  price: level.price,
@@ -368,7 +388,9 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
368
388
  const data = rawData;
369
389
  const ohlcvData = data.data;
370
390
  const event = {
371
- tradingPairId: data.symbol,
391
+ // `trading_pair_id` is the pair UUID; `symbol` is the display string.
392
+ // `tradingPairId` must carry the UUID (the symbol lives in `candlestick.s`).
393
+ tradingPairId: data.trading_pair_id,
372
394
  tradingMode: data.trading_mode,
373
395
  interval: data.interval,
374
396
  candlestick: {
@@ -532,21 +554,20 @@ export function createMonacoWebSocket(baseUrl, options = {}) {
532
554
  disconnect,
533
555
  isConnected: () => ws?.readyState === WebSocket.OPEN,
534
556
  getStatus,
535
- setToken: (newToken) => {
536
- token = newToken || undefined;
537
- stopReconnect();
538
- const currentSocket = ws;
539
- if (currentSocket?.readyState === WebSocket.OPEN ||
540
- currentSocket?.readyState === WebSocket.CONNECTING ||
541
- currentSocket?.readyState === WebSocket.CLOSING) {
542
- currentSocket.close(1000, newToken ? "Token updated, reconnecting." : "Token cleared.");
543
- ws = null;
557
+ setSessionKeypair: (credentials) => {
558
+ session = credentials ?? undefined;
559
+ // If already connected, (re)send the signed handshake so the connection
560
+ // picks up the new session without tearing down. If not yet connected,
561
+ // onopen will send it. Clearing the session leaves the connection up for
562
+ // public channels; logout calls disconnect() to fully tear down.
563
+ if (session && ws?.readyState === WebSocket.OPEN) {
564
+ sendAuthenticate();
565
+ }
566
+ else if (session && (!ws || ws.readyState === WebSocket.CLOSED)) {
567
+ connect().catch((err) => {
568
+ console.warn("WebSocket: Failed to connect after session update:", err);
569
+ });
544
570
  }
545
- if (!newToken)
546
- return;
547
- connect().catch((err) => {
548
- console.warn("WebSocket: Failed to reconnect after token update:", err);
549
- });
550
571
  },
551
572
  orders: subscribeOrders,
552
573
  orderbook: subscribeOrderbook,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Whitelist API Implementation
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
+ * ```
16
+ */
17
+ import type { SubmitWhitelistRequest, SubmitWhitelistResponse, WhitelistAPI } from "@0xmonaco/types";
18
+ import { BaseAPI } from "../base";
19
+ export declare class WhitelistAPIImpl extends BaseAPI implements WhitelistAPI {
20
+ /**
21
+ * Submits a whitelist (waitlist) application. Public — no auth required.
22
+ *
23
+ * @param body - Applicant details
24
+ * @returns Promise resolving to the status message and created user id
25
+ */
26
+ submit(body: SubmitWhitelistRequest): Promise<SubmitWhitelistResponse>;
27
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Whitelist API Implementation
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
+ * ```
16
+ */
17
+ import { BaseAPI } from "../base";
18
+ import { perpRoutes } from "../perp/routes";
19
+ export class WhitelistAPIImpl extends BaseAPI {
20
+ /**
21
+ * Submits a whitelist (waitlist) application. Public — no auth required.
22
+ *
23
+ * @param body - Applicant details
24
+ * @returns Promise resolving to the status message and created user id
25
+ */
26
+ async submit(body) {
27
+ return await this.makePublicRequest(perpRoutes.whitelist.submit(), {
28
+ method: "POST",
29
+ body: JSON.stringify(body),
30
+ });
31
+ }
32
+ }
@@ -0,0 +1 @@
1
+ export { WhitelistAPIImpl } from "./api";
@@ -0,0 +1 @@
1
+ export { WhitelistAPIImpl } from "./api";
@@ -0,0 +1,18 @@
1
+ import type { InitiateWithdrawalRequest, ListPendingWithdrawalsParams, ListPendingWithdrawalsResponse, WithdrawalResponse, WithdrawalsAPI } from "@0xmonaco/types";
2
+ import { BaseAPI } from "../base";
3
+ /**
4
+ * Low-level withdrawals client.
5
+ *
6
+ * `initiateWithdrawal` debits the caller's balance via the matching engine and
7
+ * allocates a `withdrawalIndex` (its `calldata` is empty — the proof is not
8
+ * available until the withdrawal root is confirmed on-chain). `getWithdrawal`
9
+ * returns the `executeWithdrawal` calldata once that proof is ready, and throws
10
+ * an `APIError` (404 not-persisted-yet / 409 not-confirmed-yet) while it is not.
11
+ * Neither submits on-chain — use the high-level vault API, which polls for the
12
+ * calldata and broadcasts the transaction.
13
+ */
14
+ export declare class WithdrawalsAPIImpl extends BaseAPI implements WithdrawalsAPI {
15
+ initiateWithdrawal(request: InitiateWithdrawalRequest): Promise<WithdrawalResponse>;
16
+ getWithdrawal(withdrawalIndex: number): Promise<WithdrawalResponse>;
17
+ listPendingWithdrawals(params?: ListPendingWithdrawalsParams): Promise<ListPendingWithdrawalsResponse>;
18
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseAPI } from "../base";
2
+ import { perpRoutes } from "../perp";
3
+ /**
4
+ * Low-level withdrawals client.
5
+ *
6
+ * `initiateWithdrawal` debits the caller's balance via the matching engine and
7
+ * allocates a `withdrawalIndex` (its `calldata` is empty — the proof is not
8
+ * available until the withdrawal root is confirmed on-chain). `getWithdrawal`
9
+ * returns the `executeWithdrawal` calldata once that proof is ready, and throws
10
+ * an `APIError` (404 not-persisted-yet / 409 not-confirmed-yet) while it is not.
11
+ * Neither submits on-chain — use the high-level vault API, which polls for the
12
+ * calldata and broadcasts the transaction.
13
+ */
14
+ export class WithdrawalsAPIImpl extends BaseAPI {
15
+ async initiateWithdrawal(request) {
16
+ return this.makeAuthenticatedRequest(perpRoutes.withdrawals.initiate(), {
17
+ method: "POST",
18
+ body: JSON.stringify({
19
+ assetId: request.assetId,
20
+ amount: request.amount,
21
+ destination: request.destination,
22
+ source: request.source ?? "spot",
23
+ }),
24
+ });
25
+ }
26
+ async getWithdrawal(withdrawalIndex) {
27
+ // Public lookup — no auth required.
28
+ return this.makePublicRequest(perpRoutes.withdrawals.byIndex(withdrawalIndex));
29
+ }
30
+ async listPendingWithdrawals(params) {
31
+ // The gateway serves camelCase (the REST wire format), which matches the
32
+ // public surface, so the response is returned directly with no remapping.
33
+ return this.makeAuthenticatedRequest(perpRoutes.withdrawals.list({ page: params?.page, pageSize: params?.pageSize }));
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export * from "./api";
@@ -0,0 +1 @@
1
+ export * from "./api";