pmxtjs 2.48.6 → 2.49.0

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 (74) hide show
  1. package/dist/esm/generated/src/models/OrderLevel.d.ts +6 -0
  2. package/dist/esm/generated/src/models/OrderLevel.js +2 -0
  3. package/dist/esm/index.d.ts +2 -0
  4. package/dist/esm/index.js +1 -0
  5. package/dist/esm/pmxt/client.d.ts +106 -5
  6. package/dist/esm/pmxt/client.js +400 -6
  7. package/dist/esm/pmxt/constants.d.ts +11 -0
  8. package/dist/esm/pmxt/constants.js +13 -0
  9. package/dist/esm/pmxt/errors.d.ts +3 -0
  10. package/dist/esm/pmxt/errors.js +9 -0
  11. package/dist/esm/pmxt/escrow.d.ts +39 -0
  12. package/dist/esm/pmxt/escrow.js +78 -0
  13. package/dist/esm/pmxt/feed-client.d.ts +3 -0
  14. package/dist/esm/pmxt/feed-client.js +11 -2
  15. package/dist/esm/pmxt/hosted-errors.d.ts +84 -0
  16. package/dist/esm/pmxt/hosted-errors.js +186 -0
  17. package/dist/esm/pmxt/hosted-mappers.d.ts +45 -0
  18. package/dist/esm/pmxt/hosted-mappers.js +291 -0
  19. package/dist/esm/pmxt/hosted-routing.d.ts +69 -0
  20. package/dist/esm/pmxt/hosted-routing.js +119 -0
  21. package/dist/esm/pmxt/hosted-typed-data.d.ts +36 -0
  22. package/dist/esm/pmxt/hosted-typed-data.js +580 -0
  23. package/dist/esm/pmxt/models.d.ts +46 -8
  24. package/dist/esm/pmxt/server-manager.d.ts +4 -0
  25. package/dist/esm/pmxt/server-manager.js +6 -0
  26. package/dist/esm/pmxt/signers.d.ts +57 -0
  27. package/dist/esm/pmxt/signers.js +50 -0
  28. package/dist/esm/pmxt/ws-client.js +2 -1
  29. package/dist/generated/src/models/OrderLevel.d.ts +6 -0
  30. package/dist/generated/src/models/OrderLevel.js +2 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/pmxt/client.d.ts +106 -5
  34. package/dist/pmxt/client.js +399 -5
  35. package/dist/pmxt/constants.d.ts +11 -0
  36. package/dist/pmxt/constants.js +14 -1
  37. package/dist/pmxt/errors.d.ts +3 -0
  38. package/dist/pmxt/errors.js +11 -1
  39. package/dist/pmxt/escrow.d.ts +39 -0
  40. package/dist/pmxt/escrow.js +82 -0
  41. package/dist/pmxt/feed-client.d.ts +3 -0
  42. package/dist/pmxt/feed-client.js +11 -2
  43. package/dist/pmxt/hosted-errors.d.ts +84 -0
  44. package/dist/pmxt/hosted-errors.js +201 -0
  45. package/dist/pmxt/hosted-mappers.d.ts +45 -0
  46. package/dist/pmxt/hosted-mappers.js +302 -0
  47. package/dist/pmxt/hosted-routing.d.ts +69 -0
  48. package/dist/pmxt/hosted-routing.js +126 -0
  49. package/dist/pmxt/hosted-typed-data.d.ts +36 -0
  50. package/dist/pmxt/hosted-typed-data.js +619 -0
  51. package/dist/pmxt/models.d.ts +46 -8
  52. package/dist/pmxt/server-manager.d.ts +4 -0
  53. package/dist/pmxt/server-manager.js +6 -0
  54. package/dist/pmxt/signers.d.ts +57 -0
  55. package/dist/pmxt/signers.js +55 -0
  56. package/dist/pmxt/ws-client.js +2 -1
  57. package/generated/docs/OrderLevel.md +2 -0
  58. package/generated/package.json +1 -1
  59. package/generated/src/models/OrderLevel.ts +8 -0
  60. package/index.ts +1 -0
  61. package/package.json +11 -2
  62. package/pmxt/client.ts +495 -9
  63. package/pmxt/constants.ts +15 -0
  64. package/pmxt/errors.ts +11 -0
  65. package/pmxt/escrow.ts +93 -0
  66. package/pmxt/feed-client.ts +14 -2
  67. package/pmxt/hosted-errors.ts +216 -0
  68. package/pmxt/hosted-mappers.ts +312 -0
  69. package/pmxt/hosted-routing.ts +165 -0
  70. package/pmxt/hosted-typed-data.ts +767 -0
  71. package/pmxt/models.ts +65 -8
  72. package/pmxt/server-manager.ts +7 -0
  73. package/pmxt/signers.ts +86 -0
  74. package/pmxt/ws-client.ts +2 -1
package/pmxt/client.ts CHANGED
@@ -50,10 +50,44 @@ import {
50
50
 
51
51
  import { ServerManager } from "./server-manager.js";
52
52
  import { buildArgsWithOptionalOptions } from "./args.js";
53
- import { PmxtError, fromServerError } from "./errors.js";
53
+ import { PmxtError, fromServerError, InvalidOrder, NotSupported } from "./errors.js";
54
54
  import { LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js";
55
55
  import { SidecarWsClient } from "./ws-client.js";
56
56
 
57
+ // Hosted-mode trading dispatch.
58
+ // These modules are introduced as part of the hosted trading mode rollout.
59
+ // Some of them may be authored by parallel agents; until they all land, the
60
+ // import names below are the conventional ones from the plan. Cross-module
61
+ // "Cannot find module" errors during the parallel landing window resolve
62
+ // once the matching files exist.
63
+ import {
64
+ HOSTED_TRADING_VENUES,
65
+ _tradingRequest,
66
+ resolveWalletAddress,
67
+ ensureHostedTradingSupported,
68
+ formatRoutePath,
69
+ HOSTED_METHOD_ROUTES,
70
+ } from "./hosted-routing.js";
71
+ import {
72
+ orderFromV0,
73
+ positionFromV0,
74
+ balanceFromV0,
75
+ userTradeFromV0,
76
+ to6dec,
77
+ } from "./hosted-mappers.js";
78
+ import {
79
+ validateTypedData,
80
+ validateEconomics,
81
+ verifySignature,
82
+ } from "./hosted-typed-data.js";
83
+ import type { Signer, TypedData } from "./signers.js";
84
+ import { signerFromPrivateKey, EthersSigner } from "./signers.js";
85
+ import { Escrow } from "./escrow.js";
86
+ import {
87
+ MissingWalletAddress,
88
+ InvalidSignature as HostedInvalidSignature,
89
+ } from "./hosted-errors.js";
90
+
57
91
  interface RawWebSocketLike {
58
92
  send(data: string): void;
59
93
  }
@@ -236,6 +270,20 @@ export interface ExchangeOptions {
236
270
 
237
271
  /** Optional signature type (0=EOA, 1=Proxy) */
238
272
  signatureType?: number;
273
+
274
+ /**
275
+ * EVM wallet address used for hosted reads/writes. Required for hosted
276
+ * endpoints that operate on a wallet (balances, positions, trades, open
277
+ * orders). When omitted, hosted reads raise {@link MissingWalletAddress}.
278
+ */
279
+ walletAddress?: string;
280
+
281
+ /**
282
+ * External signer used for hosted writes. When `privateKey` is supplied
283
+ * without `signer` in hosted mode, an internal {@link EthersSigner} is
284
+ * built from it lazily.
285
+ */
286
+ signer?: Signer;
239
287
  }
240
288
 
241
289
  /**
@@ -252,10 +300,13 @@ export abstract class Exchange {
252
300
  "opinion",
253
301
  ]);
254
302
 
255
- protected exchangeName: string;
303
+ // Public so structural interfaces like `HostedClientLike`
304
+ // (./hosted-routing) can read the venue name and hosted credentials
305
+ // without violating protected-access on this base class.
306
+ public exchangeName: string;
307
+ public pmxtApiKey?: string;
256
308
  protected apiKey?: string;
257
309
  protected privateKey?: string;
258
- protected pmxtApiKey?: string;
259
310
  protected proxyAddress?: string;
260
311
  protected signatureType?: number;
261
312
  protected api: DefaultApi;
@@ -263,6 +314,15 @@ export abstract class Exchange {
263
314
  protected serverManager: ServerManager;
264
315
  protected initPromise: Promise<void>;
265
316
  protected isHosted: boolean;
317
+
318
+ /** Wallet address used for hosted endpoints that operate on a wallet. */
319
+ public walletAddress?: string;
320
+
321
+ /** External signer used for hosted writes. */
322
+ public signer?: Signer;
323
+
324
+ /** Escrow namespace — populated in hosted mode for trading-allowlisted venues. */
325
+ public escrow?: Escrow;
266
326
  private _hostedAccount?: { depositWallet?: string; signatureType?: number };
267
327
  private _accountDiscoveryPromise?: Promise<void>;
268
328
 
@@ -285,6 +345,8 @@ export abstract class Exchange {
285
345
  this.privateKey = options.privateKey;
286
346
  this.proxyAddress = options.proxyAddress;
287
347
  this.signatureType = options.signatureType;
348
+ this.walletAddress = options.walletAddress;
349
+ this.signer = options.signer;
288
350
 
289
351
  // Resolve base URL + hosted API key via the shared precedence
290
352
  // rules. See constants.ts for the full resolution table.
@@ -296,6 +358,27 @@ export abstract class Exchange {
296
358
  this.pmxtApiKey = resolved.pmxtApiKey;
297
359
  this.isHosted = resolved.isHosted;
298
360
 
361
+ // Hosted trading bridge: if the caller passed a privateKey but no
362
+ // explicit signer, lazily wrap it in an EthersSigner so that
363
+ // `pmxt.Polymarket(pmxtApiKey, privateKey)` just works without the
364
+ // user touching `signer`. EthersSigner's constructor synchronously
365
+ // builds an ethers.Wallet, so this remains a synchronous bridge —
366
+ // the constructor stays sync.
367
+ if (this.pmxtApiKey && this.privateKey && !this.signer) {
368
+ try {
369
+ this.signer = new EthersSigner(this.privateKey);
370
+ } catch {
371
+ // ethers not installed — defer the error to the first
372
+ // hosted write that actually needs the signer. Read-only
373
+ // hosted callers don't need ethers.
374
+ }
375
+ }
376
+
377
+ // Instantiate Escrow namespace for hosted-trading-allowlisted venues.
378
+ if (this.pmxtApiKey && HOSTED_TRADING_VENUES.has(this.exchangeName)) {
379
+ this.escrow = new Escrow(this);
380
+ }
381
+
299
382
  // auto_start_server defaults: true for local, false for hosted.
300
383
  // An explicit value in the options always wins.
301
384
  const autoStartServer = options.autoStartServer !== undefined
@@ -389,6 +472,29 @@ export abstract class Exchange {
389
472
  return headers;
390
473
  }
391
474
 
475
+ /**
476
+ * Returns true when this client should dispatch trading methods through
477
+ * the hosted PMXT trading API (`pmxtApiKey` set AND venue is on the
478
+ * hosted-trading allowlist). Used to gate every Group A method.
479
+ */
480
+ protected isHostedTradingMode(): boolean {
481
+ return Boolean(this.pmxtApiKey) && HOSTED_TRADING_VENUES.has(this.exchangeName);
482
+ }
483
+
484
+ /**
485
+ * Require a configured signer for a hosted write. Returns the signer or
486
+ * throws {@link MissingWalletAddress} (consistent with the error class
487
+ * surfaced for missing wallet wiring on hosted writes).
488
+ */
489
+ protected requireHostedSigner(): Signer {
490
+ if (!this.signer) {
491
+ throw new MissingWalletAddress(
492
+ "hosted write requires a signer (pass `signer` or `privateKey`)",
493
+ );
494
+ }
495
+ return this.signer;
496
+ }
497
+
392
498
  /**
393
499
  * Resolve the current sidecar base URL.
394
500
  *
@@ -1012,6 +1118,9 @@ export abstract class Exchange {
1012
1118
  }
1013
1119
 
1014
1120
  async submitOrder(built: BuiltOrder): Promise<Order> {
1121
+ if (this.isHostedTradingMode()) {
1122
+ return this._hostedSubmitOrder(built);
1123
+ }
1015
1124
  await this.initPromise;
1016
1125
  try {
1017
1126
  const args: any[] = [];
@@ -1037,7 +1146,88 @@ export abstract class Exchange {
1037
1146
  }
1038
1147
  }
1039
1148
 
1149
+ /**
1150
+ * Hosted-mode submitOrder: validate the stored build response, sign the
1151
+ * typed_data (and pull_typed_data for Opinion cross-chain sells), then
1152
+ * POST to `/v0/trade/submit-order`.
1153
+ */
1154
+ private async _hostedSubmitOrder(built: BuiltOrder): Promise<Order> {
1155
+ const signer = this.requireHostedSigner();
1156
+ if (!this.walletAddress) {
1157
+ throw new MissingWalletAddress(
1158
+ "hosted submitOrder requires walletAddress",
1159
+ );
1160
+ }
1161
+ // BuiltOrder is the SDK-side wrapper around the build response —
1162
+ // expect typed_data, optional pull_typed_data, built_order_id, and
1163
+ // the originating build_request to be present.
1164
+ const payload = built as unknown as Record<string, unknown>;
1165
+ const typedData = payload["typed_data"] as TypedData | undefined;
1166
+ if (!typedData) {
1167
+ throw new HostedInvalidSignature(0, "typed_data missing from built order");
1168
+ }
1169
+ const buildRequest = (payload["build_request"] as Record<string, unknown> | undefined)
1170
+ ?? ((payload["params"] as Record<string, unknown> | undefined)?.["build_request"] as Record<string, unknown> | undefined);
1171
+
1172
+ const side = String(buildRequest?.["side"] ?? "buy");
1173
+ const primaryRoute = this._hostedTypedDataRoute(side, false);
1174
+ // Layer 1: schema, Layer 2: economics.
1175
+ validateTypedData(typedData, primaryRoute, this.walletAddress);
1176
+ if (buildRequest) {
1177
+ validateEconomics(typedData, primaryRoute, buildRequest, payload);
1178
+ }
1179
+
1180
+ const signature = await signer.signTypedData(typedData);
1181
+ // Layer 3: post-sign recovery + canonical check.
1182
+ verifySignature(typedData, signature, signer.address);
1183
+
1184
+ const body: Record<string, unknown> = {
1185
+ built_order_id: payload["built_order_id"],
1186
+ signature,
1187
+ };
1188
+
1189
+ const pullTypedData = payload["pull_typed_data"] as TypedData | undefined;
1190
+ if (pullTypedData) {
1191
+ const pullRoute = this._hostedTypedDataRoute(side, true);
1192
+ if (pullRoute) {
1193
+ validateTypedData(pullTypedData, pullRoute, this.walletAddress);
1194
+ }
1195
+ const pullSig = await signer.signTypedData(pullTypedData);
1196
+ verifySignature(pullTypedData, pullSig, signer.address);
1197
+ body["pull_signature"] = pullSig;
1198
+ }
1199
+
1200
+ const route = HOSTED_METHOD_ROUTES.get("submitOrder")!;
1201
+ const data = await _tradingRequest(this, { method: route.method, path: route.path, body });
1202
+ return orderFromV0(data as Record<string, unknown>);
1203
+ }
1204
+
1205
+ /**
1206
+ * Resolve the per-(venue, side, pull) typed-data schema route used by
1207
+ * `validateTypedData` / `validateEconomics`. Returns undefined for the
1208
+ * pull leg when a venue/side combo doesn't have one.
1209
+ */
1210
+ private _hostedTypedDataRoute(side: string, isPull: boolean): string {
1211
+ const venue = this.exchangeName;
1212
+ const sideLower = side.toLowerCase();
1213
+ if (venue === "polymarket") {
1214
+ return sideLower === "sell" ? "polymarket_sell" : "polymarket_buy";
1215
+ }
1216
+ // opinion
1217
+ if (sideLower === "buy") return "opinion_buy";
1218
+ // sell — polygon, or BSC pull leg for cross-chain
1219
+ return isPull ? "opinion_sell_bsc_pull" : "opinion_sell_polygon";
1220
+ }
1221
+
1222
+ private _hostedCancelTypedDataRoute(isPull: boolean): string {
1223
+ if (this.exchangeName === "polymarket") return "cancel_polymarket";
1224
+ return isPull ? "cancel_opinion_bsc_pull" : "cancel_opinion_polygon";
1225
+ }
1226
+
1040
1227
  async cancelOrder(orderId: string): Promise<Order> {
1228
+ if (this.isHostedTradingMode()) {
1229
+ return this._hostedCancelOrder(orderId);
1230
+ }
1041
1231
  await this.initPromise;
1042
1232
  try {
1043
1233
  const args: any[] = [];
@@ -1063,7 +1253,56 @@ export abstract class Exchange {
1063
1253
  }
1064
1254
  }
1065
1255
 
1256
+ /**
1257
+ * Hosted-mode cancelOrder: build the cancel typed_data on the server,
1258
+ * validate + sign (dual-sign for Opinion cross-chain), then submit.
1259
+ */
1260
+ private async _hostedCancelOrder(orderId: string): Promise<Order> {
1261
+ const signer = this.requireHostedSigner();
1262
+ if (!this.walletAddress) {
1263
+ throw new MissingWalletAddress(
1264
+ "hosted cancelOrder requires walletAddress",
1265
+ );
1266
+ }
1267
+
1268
+ const buildRoute = HOSTED_METHOD_ROUTES.get("cancelOrderBuild")!;
1269
+ const buildResp = await _tradingRequest(this, {
1270
+ method: buildRoute.method,
1271
+ path: buildRoute.path,
1272
+ body: { order_id: orderId },
1273
+ }) as Record<string, unknown>;
1274
+
1275
+ const typedData = buildResp["typed_data"] as TypedData | undefined;
1276
+ if (!typedData) {
1277
+ throw new HostedInvalidSignature(0, "typed_data missing from cancel build response");
1278
+ }
1279
+
1280
+ validateTypedData(typedData, this._hostedCancelTypedDataRoute(false), this.walletAddress);
1281
+ const signature = await signer.signTypedData(typedData);
1282
+ verifySignature(typedData, signature, signer.address);
1283
+
1284
+ const body: Record<string, unknown> = {
1285
+ cancel_id: buildResp["cancel_id"],
1286
+ signature,
1287
+ };
1288
+
1289
+ const pullTypedData = buildResp["pull_typed_data"] as TypedData | undefined;
1290
+ if (pullTypedData) {
1291
+ validateTypedData(pullTypedData, this._hostedCancelTypedDataRoute(true), this.walletAddress);
1292
+ const pullSig = await signer.signTypedData(pullTypedData);
1293
+ verifySignature(pullTypedData, pullSig, signer.address);
1294
+ body["pull_signature"] = pullSig;
1295
+ }
1296
+
1297
+ const route = HOSTED_METHOD_ROUTES.get("cancelOrder")!;
1298
+ const data = await _tradingRequest(this, { method: route.method, path: route.path, body });
1299
+ return orderFromV0(data as Record<string, unknown>);
1300
+ }
1301
+
1066
1302
  async fetchOrder(orderId: string): Promise<Order> {
1303
+ if (this.isHostedTradingMode()) {
1304
+ return this._hostedFetchOrder(orderId);
1305
+ }
1067
1306
  await this.initPromise;
1068
1307
  try {
1069
1308
  const args: any[] = [];
@@ -1089,7 +1328,17 @@ export abstract class Exchange {
1089
1328
  }
1090
1329
  }
1091
1330
 
1331
+ private async _hostedFetchOrder(orderId: string): Promise<Order> {
1332
+ const route = HOSTED_METHOD_ROUTES.get("fetchOrder")!;
1333
+ const path = formatRoutePath(route, { order_id: orderId });
1334
+ const data = await _tradingRequest(this, { method: route.method, path });
1335
+ return orderFromV0(data as Record<string, unknown>);
1336
+ }
1337
+
1092
1338
  async fetchOpenOrders(marketId?: string): Promise<Order[]> {
1339
+ if (this.isHostedTradingMode()) {
1340
+ return this._hostedFetchOpenOrders(marketId);
1341
+ }
1093
1342
  await this.initPromise;
1094
1343
  try {
1095
1344
  const args: any[] = [];
@@ -1115,7 +1364,24 @@ export abstract class Exchange {
1115
1364
  }
1116
1365
  }
1117
1366
 
1367
+ private async _hostedFetchOpenOrders(marketId?: string): Promise<Order[]> {
1368
+ const address = resolveWalletAddress(this, undefined);
1369
+ const route = HOSTED_METHOD_ROUTES.get("fetchOpenOrders")!;
1370
+ const params: Record<string, string> = { address };
1371
+ if (marketId !== undefined) params["market_id"] = marketId;
1372
+ const data = await _tradingRequest(this, {
1373
+ method: route.method,
1374
+ path: route.path,
1375
+ params,
1376
+ });
1377
+ const items = (Array.isArray(data) ? data : (data as Record<string, unknown>)?.["orders"] ?? []) as unknown[];
1378
+ return (items as Record<string, unknown>[]).map(orderFromV0);
1379
+ }
1380
+
1118
1381
  async fetchMyTrades(params?: MyTradesParams): Promise<UserTrade[]> {
1382
+ if (this.isHostedTradingMode()) {
1383
+ return this._hostedFetchMyTrades(params);
1384
+ }
1119
1385
  await this.initPromise;
1120
1386
  try {
1121
1387
  const args: any[] = [];
@@ -1141,7 +1407,32 @@ export abstract class Exchange {
1141
1407
  }
1142
1408
  }
1143
1409
 
1410
+ private async _hostedFetchMyTrades(params?: MyTradesParams): Promise<UserTrade[]> {
1411
+ const address = resolveWalletAddress(this, undefined);
1412
+ const route = HOSTED_METHOD_ROUTES.get("fetchMyTrades")!;
1413
+ const path = formatRoutePath(route, { address });
1414
+ const q: Record<string, string> = {};
1415
+ if (params?.marketId) q["market_id"] = params.marketId;
1416
+ if (params?.outcomeId) q["outcome_id"] = params.outcomeId;
1417
+ if (params?.limit !== undefined) q["limit"] = String(params.limit);
1418
+ if (params?.cursor) q["cursor"] = params.cursor;
1419
+ if (params?.since) q["since"] = String(params.since.getTime());
1420
+ if (params?.until) q["until"] = String(params.until.getTime());
1421
+ const data = await _tradingRequest(this, {
1422
+ method: route.method,
1423
+ path,
1424
+ params: Object.keys(q).length ? q : undefined,
1425
+ });
1426
+ const items = (Array.isArray(data) ? data : (data as Record<string, unknown>)?.["trades"] ?? []) as unknown[];
1427
+ return (items as Record<string, unknown>[]).map(userTradeFromV0);
1428
+ }
1429
+
1144
1430
  async fetchClosedOrders(params?: OrderHistoryParams): Promise<Order[]> {
1431
+ if (this.isHostedTradingMode()) {
1432
+ throw new NotSupported(
1433
+ "Settled orders are modeled as trades — use fetchMyTrades().",
1434
+ );
1435
+ }
1145
1436
  await this.initPromise;
1146
1437
  try {
1147
1438
  const args: any[] = [];
@@ -1168,6 +1459,11 @@ export abstract class Exchange {
1168
1459
  }
1169
1460
 
1170
1461
  async fetchAllOrders(params?: OrderHistoryParams): Promise<Order[]> {
1462
+ if (this.isHostedTradingMode()) {
1463
+ throw new NotSupported(
1464
+ "Use fetchOpenOrders() and fetchMyTrades() separately.",
1465
+ );
1466
+ }
1171
1467
  await this.initPromise;
1172
1468
  try {
1173
1469
  const args: any[] = [];
@@ -1194,6 +1490,9 @@ export abstract class Exchange {
1194
1490
  }
1195
1491
 
1196
1492
  async fetchPositions(address?: string): Promise<Position[]> {
1493
+ if (this.isHostedTradingMode()) {
1494
+ return this._hostedFetchPositions(address);
1495
+ }
1197
1496
  await this.initPromise;
1198
1497
  try {
1199
1498
  const args: any[] = [];
@@ -1219,7 +1518,19 @@ export abstract class Exchange {
1219
1518
  }
1220
1519
  }
1221
1520
 
1521
+ private async _hostedFetchPositions(address?: string): Promise<Position[]> {
1522
+ const resolvedAddr = resolveWalletAddress(this, address);
1523
+ const route = HOSTED_METHOD_ROUTES.get("fetchPositions")!;
1524
+ const path = formatRoutePath(route, { address: resolvedAddr });
1525
+ const data = await _tradingRequest(this, { method: route.method, path });
1526
+ const items = (Array.isArray(data) ? data : (data as Record<string, unknown>)?.["positions"] ?? []) as unknown[];
1527
+ return (items as Record<string, unknown>[]).map(positionFromV0);
1528
+ }
1529
+
1222
1530
  async fetchBalance(address?: string): Promise<Balance[]> {
1531
+ if (this.isHostedTradingMode()) {
1532
+ return this._hostedFetchBalance(address);
1533
+ }
1223
1534
  await this.initPromise;
1224
1535
  try {
1225
1536
  const args: any[] = [];
@@ -1245,6 +1556,19 @@ export abstract class Exchange {
1245
1556
  }
1246
1557
  }
1247
1558
 
1559
+ private async _hostedFetchBalance(address?: string): Promise<Balance[]> {
1560
+ const resolvedAddr = resolveWalletAddress(this, address);
1561
+ const route = HOSTED_METHOD_ROUTES.get("fetchBalance")!;
1562
+ const path = formatRoutePath(route, { address: resolvedAddr });
1563
+ const data = await _tradingRequest(this, { method: route.method, path });
1564
+ // Hosted balance is a single USDC escrow record; wrap in an array
1565
+ // to match the existing Balance[] return shape.
1566
+ if (Array.isArray(data)) {
1567
+ return (data as Record<string, unknown>[]).map(balanceFromV0);
1568
+ }
1569
+ return [balanceFromV0(data as Record<string, unknown>)];
1570
+ }
1571
+
1248
1572
  async unwatchOrderBook(outcomeId: string | MarketOutcome): Promise<void> {
1249
1573
  await this.initPromise;
1250
1574
  try {
@@ -1938,6 +2262,9 @@ export abstract class Exchange {
1938
2262
  * ```
1939
2263
  */
1940
2264
  async buildOrder(params: CreateOrderParams & { outcome?: MarketOutcome }): Promise<BuiltOrder> {
2265
+ if (this.isHostedTradingMode()) {
2266
+ return this._hostedBuildOrder(params);
2267
+ }
1941
2268
  if (this.isHosted) {
1942
2269
  throw new PmxtError(
1943
2270
  "Trade execution is not available through the hosted API. " +
@@ -2069,6 +2396,122 @@ export abstract class Exchange {
2069
2396
  return convertOrder(submitData);
2070
2397
  }
2071
2398
 
2399
+ /**
2400
+ * Hosted-mode buildOrder: validate inputs locally, then POST to the
2401
+ * trading service's `build-order` endpoint and return a BuiltOrder
2402
+ * that carries the original build_request for Layer-2 economic checks
2403
+ * at submit time.
2404
+ */
2405
+ private async _hostedBuildOrder(
2406
+ params: CreateOrderParams & { outcome?: MarketOutcome },
2407
+ ): Promise<BuiltOrder> {
2408
+ const body = this._hostedBuildOrderBody(params);
2409
+ const route = HOSTED_METHOD_ROUTES.get("buildOrder")!;
2410
+ const data = await _tradingRequest(this, {
2411
+ method: route.method,
2412
+ path: route.path,
2413
+ body,
2414
+ }) as Record<string, unknown>;
2415
+ // Attach the originating build_request so submit can run economic
2416
+ // validation without an extra catalog round-trip.
2417
+ const built = { ...data, build_request: body } as unknown as BuiltOrder;
2418
+ return built;
2419
+ }
2420
+
2421
+ /**
2422
+ * Hosted-mode createOrder: build → sign → submit single-call wrapper.
2423
+ */
2424
+ private async _hostedCreateOrder(params: any): Promise<Order> {
2425
+ const built = await this._hostedBuildOrder(params);
2426
+ return this._hostedSubmitOrder(built);
2427
+ }
2428
+
2429
+ /**
2430
+ * Construct the hosted build-order request body and validate inputs
2431
+ * locally per the v0 contract (denom/side compatibility, > 6-decimal
2432
+ * precision rejected via {@link to6dec}).
2433
+ */
2434
+ private _hostedBuildOrderBody(
2435
+ params: CreateOrderParams & { outcome?: MarketOutcome },
2436
+ ): Record<string, unknown> {
2437
+ let marketId = params.marketId;
2438
+ let outcomeId = params.outcomeId;
2439
+
2440
+ if (params.outcome) {
2441
+ if (marketId !== undefined || outcomeId !== undefined) {
2442
+ throw new InvalidOrder(
2443
+ "cannot specify both 'outcome' and 'marketId'/'outcomeId'",
2444
+ );
2445
+ }
2446
+ const outcome: MarketOutcome = params.outcome;
2447
+ if (!outcome.marketId) {
2448
+ throw new InvalidOrder(
2449
+ "outcome.marketId is not set; ensure the outcome comes from a fetched market",
2450
+ );
2451
+ }
2452
+ marketId = outcome.marketId;
2453
+ outcomeId = outcome.outcomeId;
2454
+ }
2455
+
2456
+ const side = String(params.side);
2457
+ const orderType = String(params.type ?? "market");
2458
+ const denom = (params as unknown as Record<string, unknown>)["denom"] as
2459
+ | "usdc"
2460
+ | "shares"
2461
+ | undefined;
2462
+
2463
+ // denom/side compatibility per v0:
2464
+ // market buy -> denom='usdc'
2465
+ // market sell -> denom='shares'
2466
+ // any limit -> denom='shares'
2467
+ let resolvedDenom: "usdc" | "shares";
2468
+ if (orderType === "market") {
2469
+ if (side === "buy") {
2470
+ if (denom && denom !== "usdc") {
2471
+ throw new InvalidOrder("market buy requires denom='usdc'");
2472
+ }
2473
+ resolvedDenom = "usdc";
2474
+ } else if (side === "sell") {
2475
+ if (denom && denom !== "shares") {
2476
+ throw new InvalidOrder("market sell requires denom='shares'");
2477
+ }
2478
+ resolvedDenom = "shares";
2479
+ } else {
2480
+ throw new InvalidOrder(`unknown side: ${side}`);
2481
+ }
2482
+ } else {
2483
+ if (denom && denom !== "shares") {
2484
+ throw new InvalidOrder("limit orders require denom='shares'");
2485
+ }
2486
+ resolvedDenom = "shares";
2487
+ }
2488
+
2489
+ if (!(Number(params.amount) > 0)) {
2490
+ throw new InvalidOrder("amount must be positive");
2491
+ }
2492
+
2493
+ // to6dec throws InvalidOrder for sub-micro precision.
2494
+ const amount6dec = to6dec(params.amount as number).toString();
2495
+
2496
+ const body: Record<string, unknown> = {
2497
+ market_id: marketId,
2498
+ outcome_id: outcomeId,
2499
+ side,
2500
+ order_type: orderType,
2501
+ denom: resolvedDenom,
2502
+ amount: params.amount,
2503
+ amount_6dec: amount6dec,
2504
+ };
2505
+
2506
+ if (params.price !== undefined) body["price"] = params.price;
2507
+ const extra = params as unknown as Record<string, unknown>;
2508
+ if (extra["slippage_pct"] !== undefined) {
2509
+ body["slippage_pct"] = extra["slippage_pct"];
2510
+ }
2511
+ if (this.walletAddress) body["user_address"] = this.walletAddress;
2512
+ return body;
2513
+ }
2514
+
2072
2515
  /**
2073
2516
  * @example
2074
2517
  * ```typescript
@@ -2082,11 +2525,16 @@ export abstract class Exchange {
2082
2525
  * });
2083
2526
  * ```
2084
2527
  */
2085
- async createOrder(params: any): Promise<Order> {
2528
+ async createOrder(params: CreateOrderParams & { outcome?: MarketOutcome }): Promise<Order> {
2529
+ // SOR escape path (preserved): legacy hosted SOR flow uses a venue-side
2530
+ // SDK to execute the legs, only when a privateKey is present.
2531
+ if (this.isHosted && this.exchangeName === 'sor' && this.privateKey) {
2532
+ return this._executeSorOrder(params as any);
2533
+ }
2534
+ if (this.isHostedTradingMode()) {
2535
+ return this._hostedCreateOrder(params);
2536
+ }
2086
2537
  if (this.isHosted) {
2087
- if (this.exchangeName === 'sor' && this.privateKey) {
2088
- return this._executeSorOrder(params);
2089
- }
2090
2538
  throw new PmxtError(
2091
2539
  "Trade execution is not available through the hosted API. " +
2092
2540
  "Use the local PMXT SDK with your venue credentials instead. " +
@@ -2821,18 +3269,56 @@ export class Hyperliquid extends Exchange {
2821
3269
  }
2822
3270
  }
2823
3271
 
3272
+ /**
3273
+ * Options for the SuiBets exchange client.
3274
+ */
3275
+ export interface SuiBetsOptions extends ExchangeOptions {
3276
+ /**
3277
+ * Sui wallet address (0x + 64 hex chars).
3278
+ * Required for fetchPositions(). Can also be set via the
3279
+ * SUIBETS_WALLET_ADDRESS environment variable on the sidecar.
3280
+ */
3281
+ walletAddress?: string;
3282
+ }
3283
+
2824
3284
  /**
2825
3285
  * SuiBets exchange client.
2826
3286
  *
3287
+ * SuiBets is a decentralised P2P sports betting exchange on Sui mainnet.
3288
+ * No house edge. 2% platform fee.
3289
+ * Contract: 0xd51fe151bec66a15b086a67c1cfce9b05759ddac1d73fcd3e14324ad202b2e59
3290
+ *
2827
3291
  * @example
2828
3292
  * ```typescript
2829
3293
  * const suibets = new SuiBets();
2830
- * const markets = await suibets.fetchMarkets();
3294
+ * const markets = await suibets.fetchMarkets({ limit: 20 });
3295
+ *
3296
+ * // With wallet for fetchPositions()
3297
+ * const me = new SuiBets({ walletAddress: '0xabc...' });
3298
+ * const positions = await me.fetchPositions();
2831
3299
  * ```
2832
3300
  */
2833
3301
  export class SuiBets extends Exchange {
2834
- constructor(options: ExchangeOptions = {}) {
3302
+ private readonly _walletAddress?: string;
3303
+
3304
+ constructor(options: SuiBetsOptions = {}) {
2835
3305
  super("suibets", options);
3306
+ this._walletAddress = options.walletAddress;
3307
+ }
3308
+
3309
+ /**
3310
+ * Includes walletAddress in the credentials sent to the sidecar so
3311
+ * that fetchPositions() can reach the /api/p2p/my endpoint.
3312
+ * Falls back to SUIBETS_WALLET_ADDRESS env var on the sidecar side
3313
+ * when walletAddress is not set here.
3314
+ */
3315
+ protected override getCredentials(): ExchangeCredentials | undefined {
3316
+ const base = super.getCredentials();
3317
+ if (!this._walletAddress) return base;
3318
+ return {
3319
+ ...(base ?? {}),
3320
+ walletAddress: this._walletAddress,
3321
+ } as ExchangeCredentials & { walletAddress: string };
2836
3322
  }
2837
3323
  }
2838
3324
 
package/pmxt/constants.ts CHANGED
@@ -65,3 +65,18 @@ export function resolvePmxtBaseUrl(args: {
65
65
  if (pmxtApiKey) return pick(HOSTED_URL);
66
66
  return pick(LOCAL_URL);
67
67
  }
68
+
69
+ /**
70
+ * Lowercase 0x-prefixed escrow addresses that are pre-funded by pmxt for
71
+ * hosted trading. Orders routed through these addresses use the shared
72
+ * escrow balance rather than a per-venue deposit.
73
+ */
74
+ export const PREFUNDED_ESCROW_ADDRESSES: ReadonlySet<string> = new Set([
75
+ "0x3ad326f78b1390b9a5dc5f00e7f62f8632de23e2",
76
+ ]);
77
+
78
+ /**
79
+ * Lowercase 0x-prefixed escrow addresses that the hosted trading API treats
80
+ * as venue-owned escrows. Currently empty; populated as venues onboard.
81
+ */
82
+ export const VENUE_ESCROW_ADDRESSES: ReadonlySet<string> = new Set<string>();