@limitless-exchange/sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/index.d.mts +3015 -0
- package/dist/index.d.ts +3015 -0
- package/dist/index.js +2555 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2485 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
APIError: () => APIError,
|
|
34
|
+
AuthenticatedClient: () => AuthenticatedClient,
|
|
35
|
+
Authenticator: () => Authenticator,
|
|
36
|
+
BASE_SEPOLIA_CHAIN_ID: () => BASE_SEPOLIA_CHAIN_ID,
|
|
37
|
+
CONTRACT_ADDRESSES: () => CONTRACT_ADDRESSES,
|
|
38
|
+
ConsoleLogger: () => ConsoleLogger,
|
|
39
|
+
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
40
|
+
DEFAULT_CHAIN_ID: () => DEFAULT_CHAIN_ID,
|
|
41
|
+
DEFAULT_WS_URL: () => DEFAULT_WS_URL,
|
|
42
|
+
HttpClient: () => HttpClient,
|
|
43
|
+
MarketFetcher: () => MarketFetcher,
|
|
44
|
+
MarketType: () => MarketType,
|
|
45
|
+
MessageSigner: () => MessageSigner,
|
|
46
|
+
NoOpLogger: () => NoOpLogger,
|
|
47
|
+
OrderBuilder: () => OrderBuilder,
|
|
48
|
+
OrderClient: () => OrderClient,
|
|
49
|
+
OrderSigner: () => OrderSigner,
|
|
50
|
+
OrderType: () => OrderType,
|
|
51
|
+
PortfolioFetcher: () => PortfolioFetcher,
|
|
52
|
+
RetryConfig: () => RetryConfig,
|
|
53
|
+
RetryableClient: () => RetryableClient,
|
|
54
|
+
SIGNING_MESSAGE_TEMPLATE: () => SIGNING_MESSAGE_TEMPLATE,
|
|
55
|
+
Side: () => Side,
|
|
56
|
+
SignatureType: () => SignatureType,
|
|
57
|
+
ValidationError: () => ValidationError,
|
|
58
|
+
WebSocketClient: () => WebSocketClient,
|
|
59
|
+
WebSocketState: () => WebSocketState,
|
|
60
|
+
ZERO_ADDRESS: () => ZERO_ADDRESS,
|
|
61
|
+
getContractAddress: () => getContractAddress,
|
|
62
|
+
retryOnErrors: () => retryOnErrors,
|
|
63
|
+
validateOrderArgs: () => validateOrderArgs,
|
|
64
|
+
validateSignedOrder: () => validateSignedOrder,
|
|
65
|
+
validateUnsignedOrder: () => validateUnsignedOrder,
|
|
66
|
+
withRetry: () => withRetry
|
|
67
|
+
});
|
|
68
|
+
module.exports = __toCommonJS(index_exports);
|
|
69
|
+
|
|
70
|
+
// src/types/logger.ts
|
|
71
|
+
var NoOpLogger = class {
|
|
72
|
+
debug() {
|
|
73
|
+
}
|
|
74
|
+
info() {
|
|
75
|
+
}
|
|
76
|
+
warn() {
|
|
77
|
+
}
|
|
78
|
+
error() {
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var ConsoleLogger = class {
|
|
82
|
+
constructor(level = "info") {
|
|
83
|
+
this.level = level;
|
|
84
|
+
}
|
|
85
|
+
shouldLog(messageLevel) {
|
|
86
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
87
|
+
return levels.indexOf(messageLevel) >= levels.indexOf(this.level);
|
|
88
|
+
}
|
|
89
|
+
debug(message, meta) {
|
|
90
|
+
if (this.shouldLog("debug")) {
|
|
91
|
+
console.debug("[Limitless SDK]", message, meta || "");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
info(message, meta) {
|
|
95
|
+
if (this.shouldLog("info")) {
|
|
96
|
+
console.info("[Limitless SDK]", message, meta || "");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
warn(message, meta) {
|
|
100
|
+
if (this.shouldLog("warn")) {
|
|
101
|
+
console.warn("[Limitless SDK]", message, meta || "");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
error(message, error, meta) {
|
|
105
|
+
if (this.shouldLog("error")) {
|
|
106
|
+
const errorMsg = error ? error.message : "";
|
|
107
|
+
console.error("[Limitless SDK]", message, errorMsg ? `- ${errorMsg}` : "", meta || "");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/types/orders.ts
|
|
113
|
+
var Side = /* @__PURE__ */ ((Side2) => {
|
|
114
|
+
Side2[Side2["BUY"] = 0] = "BUY";
|
|
115
|
+
Side2[Side2["SELL"] = 1] = "SELL";
|
|
116
|
+
return Side2;
|
|
117
|
+
})(Side || {});
|
|
118
|
+
var OrderType = /* @__PURE__ */ ((OrderType3) => {
|
|
119
|
+
OrderType3["FOK"] = "FOK";
|
|
120
|
+
OrderType3["GTC"] = "GTC";
|
|
121
|
+
return OrderType3;
|
|
122
|
+
})(OrderType || {});
|
|
123
|
+
var MarketType = /* @__PURE__ */ ((MarketType3) => {
|
|
124
|
+
MarketType3["CLOB"] = "CLOB";
|
|
125
|
+
MarketType3["NEGRISK"] = "NEGRISK";
|
|
126
|
+
return MarketType3;
|
|
127
|
+
})(MarketType || {});
|
|
128
|
+
var SignatureType = /* @__PURE__ */ ((SignatureType2) => {
|
|
129
|
+
SignatureType2[SignatureType2["EOA"] = 0] = "EOA";
|
|
130
|
+
SignatureType2[SignatureType2["POLY_PROXY"] = 1] = "POLY_PROXY";
|
|
131
|
+
SignatureType2[SignatureType2["POLY_GNOSIS_SAFE"] = 2] = "POLY_GNOSIS_SAFE";
|
|
132
|
+
return SignatureType2;
|
|
133
|
+
})(SignatureType || {});
|
|
134
|
+
|
|
135
|
+
// src/types/websocket.ts
|
|
136
|
+
var WebSocketState = /* @__PURE__ */ ((WebSocketState2) => {
|
|
137
|
+
WebSocketState2["DISCONNECTED"] = "disconnected";
|
|
138
|
+
WebSocketState2["CONNECTING"] = "connecting";
|
|
139
|
+
WebSocketState2["CONNECTED"] = "connected";
|
|
140
|
+
WebSocketState2["RECONNECTING"] = "reconnecting";
|
|
141
|
+
WebSocketState2["ERROR"] = "error";
|
|
142
|
+
return WebSocketState2;
|
|
143
|
+
})(WebSocketState || {});
|
|
144
|
+
|
|
145
|
+
// src/auth/signer.ts
|
|
146
|
+
var import_ethers = require("ethers");
|
|
147
|
+
var MessageSigner = class {
|
|
148
|
+
/**
|
|
149
|
+
* Creates a new message signer instance.
|
|
150
|
+
*
|
|
151
|
+
* @param wallet - Ethers wallet instance for signing
|
|
152
|
+
*/
|
|
153
|
+
constructor(wallet) {
|
|
154
|
+
this.wallet = wallet;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Creates authentication headers for API requests.
|
|
158
|
+
*
|
|
159
|
+
* @param signingMessage - Message to sign from the API
|
|
160
|
+
* @returns Promise resolving to signature headers
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* const signer = new MessageSigner(wallet);
|
|
165
|
+
* const headers = await signer.createAuthHeaders(message);
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
async createAuthHeaders(signingMessage) {
|
|
169
|
+
const hexMessage = this.stringToHex(signingMessage);
|
|
170
|
+
const signature = await this.wallet.signMessage(signingMessage);
|
|
171
|
+
const address = this.wallet.address;
|
|
172
|
+
const recoveredAddress = import_ethers.ethers.verifyMessage(signingMessage, signature);
|
|
173
|
+
if (address.toLowerCase() !== recoveredAddress.toLowerCase()) {
|
|
174
|
+
throw new Error("Signature verification failed: address mismatch");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
"x-account": address,
|
|
178
|
+
"x-signing-message": hexMessage,
|
|
179
|
+
"x-signature": signature
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Signs EIP-712 typed data.
|
|
184
|
+
*
|
|
185
|
+
* @param domain - EIP-712 domain
|
|
186
|
+
* @param types - EIP-712 types
|
|
187
|
+
* @param value - Value to sign
|
|
188
|
+
* @returns Promise resolving to signature string
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* const signature = await signer.signTypedData(domain, types, order);
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
async signTypedData(domain, types, value) {
|
|
196
|
+
return await this.wallet.signTypedData(domain, types, value);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Gets the wallet address.
|
|
200
|
+
*
|
|
201
|
+
* @returns Ethereum address
|
|
202
|
+
*/
|
|
203
|
+
getAddress() {
|
|
204
|
+
return this.wallet.address;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Converts a string to hex format.
|
|
208
|
+
*
|
|
209
|
+
* @param text - String to convert
|
|
210
|
+
* @returns Hex string with 0x prefix
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
stringToHex(text) {
|
|
214
|
+
return import_ethers.ethers.hexlify(import_ethers.ethers.toUtf8Bytes(text));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/auth/authenticator.ts
|
|
219
|
+
var Authenticator = class {
|
|
220
|
+
/**
|
|
221
|
+
* Creates a new authenticator instance.
|
|
222
|
+
*
|
|
223
|
+
* @param httpClient - HTTP client for API requests
|
|
224
|
+
* @param signer - Message signer for wallet operations
|
|
225
|
+
* @param logger - Optional logger for debugging and monitoring (default: no logging)
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* // Without logging (default)
|
|
230
|
+
* const authenticator = new Authenticator(httpClient, signer);
|
|
231
|
+
*
|
|
232
|
+
* // With logging
|
|
233
|
+
* import { ConsoleLogger } from '@limitless-exchange/sdk';
|
|
234
|
+
* const logger = new ConsoleLogger('debug');
|
|
235
|
+
* const authenticator = new Authenticator(httpClient, signer, logger);
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
constructor(httpClient, signer, logger) {
|
|
239
|
+
this.httpClient = httpClient;
|
|
240
|
+
this.signer = signer;
|
|
241
|
+
this.logger = logger || new NoOpLogger();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Gets a signing message from the API.
|
|
245
|
+
*
|
|
246
|
+
* @returns Promise resolving to signing message string
|
|
247
|
+
* @throws Error if API request fails
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* const message = await authenticator.getSigningMessage();
|
|
252
|
+
* console.log(message);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
async getSigningMessage() {
|
|
256
|
+
this.logger.debug("Requesting signing message from API");
|
|
257
|
+
const message = await this.httpClient.get("/auth/signing-message");
|
|
258
|
+
this.logger.debug("Received signing message", { length: message.length });
|
|
259
|
+
return message;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Authenticates with the API and obtains session cookie.
|
|
263
|
+
*
|
|
264
|
+
* @param options - Login options including client type and smart wallet
|
|
265
|
+
* @returns Promise resolving to authentication result
|
|
266
|
+
* @throws Error if authentication fails or smart wallet is required but not provided
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```typescript
|
|
270
|
+
* // EOA authentication
|
|
271
|
+
* const result = await authenticator.authenticate({ client: 'eoa' });
|
|
272
|
+
*
|
|
273
|
+
* // ETHERSPOT with smart wallet
|
|
274
|
+
* const result = await authenticator.authenticate({
|
|
275
|
+
* client: 'etherspot',
|
|
276
|
+
* smartWallet: '0x...'
|
|
277
|
+
* });
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
async authenticate(options = {}) {
|
|
281
|
+
const client = options.client || "eoa";
|
|
282
|
+
this.logger.info("Starting authentication", {
|
|
283
|
+
client,
|
|
284
|
+
hasSmartWallet: !!options.smartWallet
|
|
285
|
+
});
|
|
286
|
+
if (client === "etherspot" && !options.smartWallet) {
|
|
287
|
+
this.logger.error("Smart wallet address required for ETHERSPOT client");
|
|
288
|
+
throw new Error("Smart wallet address is required for ETHERSPOT client");
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const signingMessage = await this.getSigningMessage();
|
|
292
|
+
this.logger.debug("Creating signature headers");
|
|
293
|
+
const headers = await this.signer.createAuthHeaders(signingMessage);
|
|
294
|
+
this.logger.debug("Sending authentication request", { client });
|
|
295
|
+
const response = await this.httpClient.postWithResponse(
|
|
296
|
+
"/auth/login",
|
|
297
|
+
{ client, smartWallet: options.smartWallet },
|
|
298
|
+
{
|
|
299
|
+
headers,
|
|
300
|
+
validateStatus: (status) => status < 500
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
this.logger.debug("Extracting session cookie from response");
|
|
304
|
+
const cookies = this.httpClient.extractCookies(response);
|
|
305
|
+
const sessionCookie = cookies["limitless_session"];
|
|
306
|
+
if (!sessionCookie) {
|
|
307
|
+
this.logger.error("Session cookie not found in response headers");
|
|
308
|
+
throw new Error("Failed to obtain session cookie from response");
|
|
309
|
+
}
|
|
310
|
+
this.httpClient.setSessionCookie(sessionCookie);
|
|
311
|
+
this.logger.info("Authentication successful", {
|
|
312
|
+
account: response.data.account,
|
|
313
|
+
client: response.data.client
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
sessionCookie,
|
|
317
|
+
profile: response.data
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
this.logger.error("Authentication failed", error, {
|
|
321
|
+
client
|
|
322
|
+
});
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Verifies the current authentication status.
|
|
328
|
+
*
|
|
329
|
+
* @param sessionCookie - Session cookie to verify
|
|
330
|
+
* @returns Promise resolving to user's Ethereum address
|
|
331
|
+
* @throws Error if session is invalid
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* const address = await authenticator.verifyAuth(sessionCookie);
|
|
336
|
+
* console.log(`Authenticated as: ${address}`);
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
async verifyAuth(sessionCookie) {
|
|
340
|
+
this.logger.debug("Verifying authentication session");
|
|
341
|
+
const originalCookie = this.httpClient["sessionCookie"];
|
|
342
|
+
this.httpClient.setSessionCookie(sessionCookie);
|
|
343
|
+
try {
|
|
344
|
+
const address = await this.httpClient.get("/auth/verify-auth");
|
|
345
|
+
this.logger.info("Session verified", { address });
|
|
346
|
+
return address;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
this.logger.error("Session verification failed", error);
|
|
349
|
+
throw error;
|
|
350
|
+
} finally {
|
|
351
|
+
if (originalCookie) {
|
|
352
|
+
this.httpClient.setSessionCookie(originalCookie);
|
|
353
|
+
} else {
|
|
354
|
+
this.httpClient.clearSessionCookie();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Logs out and clears the session.
|
|
360
|
+
*
|
|
361
|
+
* @param sessionCookie - Session cookie to invalidate
|
|
362
|
+
* @throws Error if logout request fails
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* await authenticator.logout(sessionCookie);
|
|
367
|
+
* console.log('Logged out successfully');
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
async logout(sessionCookie) {
|
|
371
|
+
this.logger.debug("Logging out session");
|
|
372
|
+
const originalCookie = this.httpClient["sessionCookie"];
|
|
373
|
+
this.httpClient.setSessionCookie(sessionCookie);
|
|
374
|
+
try {
|
|
375
|
+
await this.httpClient.post("/auth/logout", {});
|
|
376
|
+
this.logger.info("Logout successful");
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this.logger.error("Logout failed", error);
|
|
379
|
+
throw error;
|
|
380
|
+
} finally {
|
|
381
|
+
if (originalCookie) {
|
|
382
|
+
this.httpClient.setSessionCookie(originalCookie);
|
|
383
|
+
} else {
|
|
384
|
+
this.httpClient.clearSessionCookie();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/api/errors.ts
|
|
391
|
+
var APIError = class _APIError extends Error {
|
|
392
|
+
/**
|
|
393
|
+
* Creates a new API error.
|
|
394
|
+
*
|
|
395
|
+
* @param message - Human-readable error message
|
|
396
|
+
* @param status - HTTP status code
|
|
397
|
+
* @param data - Raw API response data
|
|
398
|
+
* @param url - Request URL
|
|
399
|
+
* @param method - Request method
|
|
400
|
+
*/
|
|
401
|
+
constructor(message, status, data, url, method) {
|
|
402
|
+
super(message);
|
|
403
|
+
this.name = "APIError";
|
|
404
|
+
this.status = status;
|
|
405
|
+
this.data = data;
|
|
406
|
+
this.url = url;
|
|
407
|
+
this.method = method;
|
|
408
|
+
if (Error.captureStackTrace) {
|
|
409
|
+
Error.captureStackTrace(this, _APIError);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Checks if this error is an authentication/authorization error.
|
|
414
|
+
*
|
|
415
|
+
* @returns True if the error is due to expired or invalid authentication
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* ```typescript
|
|
419
|
+
* try {
|
|
420
|
+
* await portfolioFetcher.getPositions();
|
|
421
|
+
* } catch (error) {
|
|
422
|
+
* if (error instanceof APIError && error.isAuthError()) {
|
|
423
|
+
* // Re-authenticate and retry
|
|
424
|
+
* await authenticator.authenticate({ client: 'eoa' });
|
|
425
|
+
* await portfolioFetcher.getPositions();
|
|
426
|
+
* }
|
|
427
|
+
* }
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
isAuthError() {
|
|
431
|
+
return this.status === 401 || this.status === 403;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// src/auth/authenticated-client.ts
|
|
436
|
+
var AuthenticatedClient = class {
|
|
437
|
+
/**
|
|
438
|
+
* Creates a new authenticated client with auto-retry capability.
|
|
439
|
+
*
|
|
440
|
+
* @param config - Configuration for authenticated client
|
|
441
|
+
*/
|
|
442
|
+
constructor(config) {
|
|
443
|
+
this.httpClient = config.httpClient;
|
|
444
|
+
this.authenticator = config.authenticator;
|
|
445
|
+
this.client = config.client;
|
|
446
|
+
this.smartWallet = config.smartWallet;
|
|
447
|
+
this.logger = config.logger || new NoOpLogger();
|
|
448
|
+
this.maxRetries = config.maxRetries ?? 1;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Executes a function with automatic retry on authentication errors.
|
|
452
|
+
*
|
|
453
|
+
* @param fn - Function to execute with auth retry
|
|
454
|
+
* @returns Promise resolving to the function result
|
|
455
|
+
* @throws Error if max retries exceeded or non-auth error occurs
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```typescript
|
|
459
|
+
* // Automatic retry on 401/403
|
|
460
|
+
* const positions = await authClient.withRetry(() =>
|
|
461
|
+
* portfolioFetcher.getPositions()
|
|
462
|
+
* );
|
|
463
|
+
*
|
|
464
|
+
* // Works with any async operation
|
|
465
|
+
* const order = await authClient.withRetry(() =>
|
|
466
|
+
* orderClient.createOrder({ ... })
|
|
467
|
+
* );
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
async withRetry(fn) {
|
|
471
|
+
let attempts = 0;
|
|
472
|
+
while (attempts <= this.maxRetries) {
|
|
473
|
+
try {
|
|
474
|
+
return await fn();
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error instanceof APIError && error.isAuthError() && attempts < this.maxRetries) {
|
|
477
|
+
this.logger.info("Authentication expired, re-authenticating...", {
|
|
478
|
+
attempt: attempts + 1,
|
|
479
|
+
maxRetries: this.maxRetries
|
|
480
|
+
});
|
|
481
|
+
await this.reauthenticate();
|
|
482
|
+
attempts++;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
throw new Error("Unexpected error: exceeded max retries");
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Re-authenticates with the API.
|
|
492
|
+
*
|
|
493
|
+
* @internal
|
|
494
|
+
*/
|
|
495
|
+
async reauthenticate() {
|
|
496
|
+
this.logger.debug("Re-authenticating with API");
|
|
497
|
+
await this.authenticator.authenticate({
|
|
498
|
+
client: this.client,
|
|
499
|
+
smartWallet: this.smartWallet
|
|
500
|
+
});
|
|
501
|
+
this.logger.info("Re-authentication successful");
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/api/http.ts
|
|
506
|
+
var import_axios = __toESM(require("axios"));
|
|
507
|
+
|
|
508
|
+
// src/utils/constants.ts
|
|
509
|
+
var DEFAULT_API_URL = "https://api.limitless.exchange";
|
|
510
|
+
var DEFAULT_WS_URL = "wss://ws.limitless.exchange";
|
|
511
|
+
var DEFAULT_CHAIN_ID = 8453;
|
|
512
|
+
var BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
513
|
+
var SIGNING_MESSAGE_TEMPLATE = "Welcome to Limitless.exchange! Please sign this message to verify your identity.\n\nNonce: {NONCE}";
|
|
514
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
515
|
+
var CONTRACT_ADDRESSES = {
|
|
516
|
+
// Base mainnet (chainId: 8453)
|
|
517
|
+
8453: {
|
|
518
|
+
CLOB: "0xa4409D988CA2218d956BeEFD3874100F444f0DC3",
|
|
519
|
+
NEGRISK: "0x5a38afc17F7E97ad8d6C547ddb837E40B4aEDfC6"
|
|
520
|
+
},
|
|
521
|
+
// Base Sepolia testnet (chainId: 84532)
|
|
522
|
+
84532: {
|
|
523
|
+
CLOB: "0x...",
|
|
524
|
+
// Add testnet addresses when available
|
|
525
|
+
NEGRISK: "0x..."
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
function getContractAddress(marketType, chainId = DEFAULT_CHAIN_ID) {
|
|
529
|
+
const addresses = CONTRACT_ADDRESSES[chainId];
|
|
530
|
+
if (!addresses) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`No contract addresses configured for chainId ${chainId}. Supported chains: ${Object.keys(CONTRACT_ADDRESSES).join(", ")}`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return addresses[marketType];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/api/http.ts
|
|
539
|
+
var HttpClient = class {
|
|
540
|
+
/**
|
|
541
|
+
* Creates a new HTTP client instance.
|
|
542
|
+
*
|
|
543
|
+
* @param config - Configuration options for the HTTP client
|
|
544
|
+
*/
|
|
545
|
+
constructor(config = {}) {
|
|
546
|
+
this.sessionCookie = config.sessionCookie;
|
|
547
|
+
this.logger = config.logger || new NoOpLogger();
|
|
548
|
+
this.client = import_axios.default.create({
|
|
549
|
+
baseURL: config.baseURL || DEFAULT_API_URL,
|
|
550
|
+
timeout: config.timeout || 3e4,
|
|
551
|
+
headers: {
|
|
552
|
+
"Content-Type": "application/json",
|
|
553
|
+
Accept: "application/json"
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
this.setupInterceptors();
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Sets up request and response interceptors.
|
|
560
|
+
* @internal
|
|
561
|
+
*/
|
|
562
|
+
setupInterceptors() {
|
|
563
|
+
this.client.interceptors.request.use(
|
|
564
|
+
(config) => {
|
|
565
|
+
if (this.sessionCookie) {
|
|
566
|
+
config.headers["Cookie"] = `limitless_session=${this.sessionCookie}`;
|
|
567
|
+
}
|
|
568
|
+
const fullUrl = `${config.baseURL || ""}${config.url || ""}`;
|
|
569
|
+
const method = config.method?.toUpperCase() || "GET";
|
|
570
|
+
this.logger.debug(`\u2192 ${method} ${fullUrl}`, {
|
|
571
|
+
headers: config.headers,
|
|
572
|
+
body: config.data
|
|
573
|
+
});
|
|
574
|
+
return config;
|
|
575
|
+
},
|
|
576
|
+
(error) => Promise.reject(error)
|
|
577
|
+
);
|
|
578
|
+
this.client.interceptors.response.use(
|
|
579
|
+
(response) => {
|
|
580
|
+
const method = response.config.method?.toUpperCase() || "GET";
|
|
581
|
+
const url = response.config.url || "";
|
|
582
|
+
this.logger.debug(`\u2713 ${response.status} ${method} ${url}`, {
|
|
583
|
+
data: response.data
|
|
584
|
+
});
|
|
585
|
+
return response;
|
|
586
|
+
},
|
|
587
|
+
(error) => {
|
|
588
|
+
if (error.response) {
|
|
589
|
+
const status = error.response.status;
|
|
590
|
+
const data = error.response.data;
|
|
591
|
+
const url = error.config?.url;
|
|
592
|
+
const method = error.config?.method?.toUpperCase();
|
|
593
|
+
let message = error.message;
|
|
594
|
+
if (data) {
|
|
595
|
+
this.logger.debug(`\u2717 ${status} ${method} ${url}`, {
|
|
596
|
+
error: data
|
|
597
|
+
});
|
|
598
|
+
if (typeof data === "object") {
|
|
599
|
+
if (Array.isArray(data.message)) {
|
|
600
|
+
const messages = data.message.map((err) => {
|
|
601
|
+
const details = Object.entries(err).filter(([_key, val]) => val !== "" && val !== null && val !== void 0).map(([key, val]) => `${key}: ${val}`).join(", ");
|
|
602
|
+
return details || JSON.stringify(err);
|
|
603
|
+
}).filter((msg) => msg.trim() !== "").join(" | ");
|
|
604
|
+
message = messages || data.error || JSON.stringify(data);
|
|
605
|
+
} else {
|
|
606
|
+
message = data.message || data.error || data.msg || data.errors && JSON.stringify(data.errors) || JSON.stringify(data);
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
message = String(data);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
throw new APIError(message, status, data, url, method);
|
|
613
|
+
} else if (error.request) {
|
|
614
|
+
throw new Error("No response received from API");
|
|
615
|
+
} else {
|
|
616
|
+
throw new Error(`Request failed: ${error.message}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Sets the session cookie for authenticated requests.
|
|
623
|
+
*
|
|
624
|
+
* @param cookie - Session cookie value
|
|
625
|
+
*/
|
|
626
|
+
setSessionCookie(cookie) {
|
|
627
|
+
this.sessionCookie = cookie;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Clears the session cookie.
|
|
631
|
+
*/
|
|
632
|
+
clearSessionCookie() {
|
|
633
|
+
this.sessionCookie = void 0;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Performs a GET request.
|
|
637
|
+
*
|
|
638
|
+
* @param url - Request URL
|
|
639
|
+
* @param config - Additional request configuration
|
|
640
|
+
* @returns Promise resolving to the response data
|
|
641
|
+
*/
|
|
642
|
+
async get(url, config) {
|
|
643
|
+
const response = await this.client.get(url, config);
|
|
644
|
+
return response.data;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Performs a POST request.
|
|
648
|
+
*
|
|
649
|
+
* @param url - Request URL
|
|
650
|
+
* @param data - Request body data
|
|
651
|
+
* @param config - Additional request configuration
|
|
652
|
+
* @returns Promise resolving to the response data
|
|
653
|
+
*/
|
|
654
|
+
async post(url, data, config) {
|
|
655
|
+
const response = await this.client.post(url, data, config);
|
|
656
|
+
return response.data;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Performs a POST request and returns the full response object.
|
|
660
|
+
* Useful when you need access to response headers (e.g., for cookie extraction).
|
|
661
|
+
*
|
|
662
|
+
* @param url - Request URL
|
|
663
|
+
* @param data - Request body data
|
|
664
|
+
* @param config - Additional request configuration
|
|
665
|
+
* @returns Promise resolving to the full AxiosResponse object
|
|
666
|
+
* @internal
|
|
667
|
+
*/
|
|
668
|
+
async postWithResponse(url, data, config) {
|
|
669
|
+
return await this.client.post(url, data, config);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Performs a DELETE request.
|
|
673
|
+
*
|
|
674
|
+
* @remarks
|
|
675
|
+
* DELETE requests typically don't have a body, so we remove the Content-Type header
|
|
676
|
+
* to avoid "Body cannot be empty" errors from the API.
|
|
677
|
+
*
|
|
678
|
+
* @param url - Request URL
|
|
679
|
+
* @param config - Additional request configuration
|
|
680
|
+
* @returns Promise resolving to the response data
|
|
681
|
+
*/
|
|
682
|
+
async delete(url, config) {
|
|
683
|
+
const deleteConfig = {
|
|
684
|
+
...config,
|
|
685
|
+
headers: {
|
|
686
|
+
...config?.headers,
|
|
687
|
+
"Content-Type": void 0
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const response = await this.client.delete(url, deleteConfig);
|
|
691
|
+
return response.data;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Extracts cookies from response headers.
|
|
695
|
+
*
|
|
696
|
+
* @param response - Axios response object
|
|
697
|
+
* @returns Object containing parsed cookies
|
|
698
|
+
* @internal
|
|
699
|
+
*/
|
|
700
|
+
extractCookies(response) {
|
|
701
|
+
const setCookie = response.headers["set-cookie"];
|
|
702
|
+
if (!setCookie) return {};
|
|
703
|
+
const cookies = {};
|
|
704
|
+
const cookieStrings = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
705
|
+
for (const cookieString of cookieStrings) {
|
|
706
|
+
const parts = cookieString.split(";")[0].split("=");
|
|
707
|
+
if (parts.length === 2) {
|
|
708
|
+
cookies[parts[0].trim()] = parts[1].trim();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return cookies;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// src/api/retry.ts
|
|
716
|
+
var RetryConfig = class {
|
|
717
|
+
/**
|
|
718
|
+
* Creates a new retry configuration.
|
|
719
|
+
*
|
|
720
|
+
* @param options - Configuration options
|
|
721
|
+
*/
|
|
722
|
+
constructor(options = {}) {
|
|
723
|
+
this.statusCodes = new Set(options.statusCodes || [429, 500, 502, 503, 504]);
|
|
724
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
725
|
+
this.delays = options.delays;
|
|
726
|
+
this.exponentialBase = options.exponentialBase ?? 2;
|
|
727
|
+
this.maxDelay = options.maxDelay ?? 60;
|
|
728
|
+
this.onRetry = options.onRetry;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Calculates delay for a given retry attempt.
|
|
732
|
+
*
|
|
733
|
+
* @param attempt - Retry attempt number (0-based)
|
|
734
|
+
* @returns Delay in seconds
|
|
735
|
+
*/
|
|
736
|
+
getDelay(attempt) {
|
|
737
|
+
if (this.delays) {
|
|
738
|
+
return this.delays[Math.min(attempt, this.delays.length - 1)];
|
|
739
|
+
} else {
|
|
740
|
+
return Math.min(Math.pow(this.exponentialBase, attempt), this.maxDelay);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
function sleep(seconds) {
|
|
745
|
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
746
|
+
}
|
|
747
|
+
function retryOnErrors(options = {}) {
|
|
748
|
+
const config = new RetryConfig(options);
|
|
749
|
+
return function(_target, _propertyKey, descriptor) {
|
|
750
|
+
const originalMethod = descriptor.value;
|
|
751
|
+
descriptor.value = async function(...args) {
|
|
752
|
+
let lastError;
|
|
753
|
+
try {
|
|
754
|
+
return await originalMethod.apply(this, args);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (error instanceof APIError && config.statusCodes.has(error.status)) {
|
|
757
|
+
lastError = error;
|
|
758
|
+
} else {
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
|
|
763
|
+
try {
|
|
764
|
+
const delay = config.getDelay(attempt);
|
|
765
|
+
if (config.onRetry && lastError) {
|
|
766
|
+
config.onRetry(attempt, lastError, delay);
|
|
767
|
+
}
|
|
768
|
+
await sleep(delay);
|
|
769
|
+
return await originalMethod.apply(this, args);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (error instanceof APIError && config.statusCodes.has(error.status)) {
|
|
772
|
+
lastError = error;
|
|
773
|
+
} else {
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
throw lastError;
|
|
779
|
+
};
|
|
780
|
+
return descriptor;
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
async function withRetry(fn, options = {}, logger = new NoOpLogger()) {
|
|
784
|
+
const config = new RetryConfig(options);
|
|
785
|
+
let lastError;
|
|
786
|
+
try {
|
|
787
|
+
return await fn();
|
|
788
|
+
} catch (error) {
|
|
789
|
+
if (error instanceof APIError && config.statusCodes.has(error.status)) {
|
|
790
|
+
lastError = error;
|
|
791
|
+
logger.warn("API error, starting retries", { status: error.status });
|
|
792
|
+
} else {
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
|
|
797
|
+
try {
|
|
798
|
+
const delay = config.getDelay(attempt);
|
|
799
|
+
if (config.onRetry && lastError) {
|
|
800
|
+
config.onRetry(attempt, lastError, delay);
|
|
801
|
+
}
|
|
802
|
+
logger.info("Retrying operation", { attempt: attempt + 1, delay });
|
|
803
|
+
await sleep(delay);
|
|
804
|
+
return await fn();
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (error instanceof APIError && config.statusCodes.has(error.status)) {
|
|
807
|
+
lastError = error;
|
|
808
|
+
logger.warn("Retry failed", { attempt: attempt + 1, status: error.status });
|
|
809
|
+
} else {
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
logger.error("All retries exhausted");
|
|
815
|
+
throw lastError;
|
|
816
|
+
}
|
|
817
|
+
var RetryableClient = class {
|
|
818
|
+
/**
|
|
819
|
+
* Creates a new retryable client wrapper.
|
|
820
|
+
*
|
|
821
|
+
* @param httpClient - HTTP client to wrap
|
|
822
|
+
* @param retryConfig - Retry configuration
|
|
823
|
+
* @param logger - Optional logger
|
|
824
|
+
*/
|
|
825
|
+
constructor(httpClient, retryConfig = new RetryConfig(), logger = new NoOpLogger()) {
|
|
826
|
+
this.httpClient = httpClient;
|
|
827
|
+
this.retryConfig = retryConfig;
|
|
828
|
+
this.logger = logger;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Performs a GET request with retry logic.
|
|
832
|
+
*
|
|
833
|
+
* @param url - Request URL
|
|
834
|
+
* @param config - Additional request configuration
|
|
835
|
+
* @returns Promise resolving to the response data
|
|
836
|
+
*/
|
|
837
|
+
async get(url, config) {
|
|
838
|
+
return withRetry(
|
|
839
|
+
async () => this.httpClient.get(url, config),
|
|
840
|
+
{
|
|
841
|
+
statusCodes: Array.from(this.retryConfig.statusCodes),
|
|
842
|
+
maxRetries: this.retryConfig.maxRetries,
|
|
843
|
+
delays: this.retryConfig.delays,
|
|
844
|
+
exponentialBase: this.retryConfig.exponentialBase,
|
|
845
|
+
maxDelay: this.retryConfig.maxDelay,
|
|
846
|
+
onRetry: this.retryConfig.onRetry
|
|
847
|
+
},
|
|
848
|
+
this.logger
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Performs a POST request with retry logic.
|
|
853
|
+
*
|
|
854
|
+
* @param url - Request URL
|
|
855
|
+
* @param data - Request body data
|
|
856
|
+
* @param config - Additional request configuration
|
|
857
|
+
* @returns Promise resolving to the response data
|
|
858
|
+
*/
|
|
859
|
+
async post(url, data, config) {
|
|
860
|
+
return withRetry(
|
|
861
|
+
async () => this.httpClient.post(url, data, config),
|
|
862
|
+
{
|
|
863
|
+
statusCodes: Array.from(this.retryConfig.statusCodes),
|
|
864
|
+
maxRetries: this.retryConfig.maxRetries,
|
|
865
|
+
delays: this.retryConfig.delays,
|
|
866
|
+
exponentialBase: this.retryConfig.exponentialBase,
|
|
867
|
+
maxDelay: this.retryConfig.maxDelay,
|
|
868
|
+
onRetry: this.retryConfig.onRetry
|
|
869
|
+
},
|
|
870
|
+
this.logger
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Performs a DELETE request with retry logic.
|
|
875
|
+
*
|
|
876
|
+
* @param url - Request URL
|
|
877
|
+
* @param config - Additional request configuration
|
|
878
|
+
* @returns Promise resolving to the response data
|
|
879
|
+
*/
|
|
880
|
+
async delete(url, config) {
|
|
881
|
+
return withRetry(
|
|
882
|
+
async () => this.httpClient.delete(url, config),
|
|
883
|
+
{
|
|
884
|
+
statusCodes: Array.from(this.retryConfig.statusCodes),
|
|
885
|
+
maxRetries: this.retryConfig.maxRetries,
|
|
886
|
+
delays: this.retryConfig.delays,
|
|
887
|
+
exponentialBase: this.retryConfig.exponentialBase,
|
|
888
|
+
maxDelay: this.retryConfig.maxDelay,
|
|
889
|
+
onRetry: this.retryConfig.onRetry
|
|
890
|
+
},
|
|
891
|
+
this.logger
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// src/orders/builder.ts
|
|
897
|
+
var import_ethers2 = require("ethers");
|
|
898
|
+
var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
|
|
899
|
+
var DEFAULT_PRICE_TICK = 1e-3;
|
|
900
|
+
var OrderBuilder = class {
|
|
901
|
+
/**
|
|
902
|
+
* Creates a new order builder instance.
|
|
903
|
+
*
|
|
904
|
+
* @param makerAddress - Ethereum address of the order maker
|
|
905
|
+
* @param feeRateBps - Fee rate in basis points (e.g., 100 = 1%)
|
|
906
|
+
* @param priceTick - Price tick size (default: 0.001 for 3 decimals)
|
|
907
|
+
*
|
|
908
|
+
* @example
|
|
909
|
+
* ```typescript
|
|
910
|
+
* const builder = new OrderBuilder(
|
|
911
|
+
* '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
912
|
+
* 300, // 3% fee
|
|
913
|
+
* 0.001 // 3 decimal price precision
|
|
914
|
+
* );
|
|
915
|
+
* ```
|
|
916
|
+
*/
|
|
917
|
+
constructor(makerAddress, feeRateBps, priceTick = DEFAULT_PRICE_TICK) {
|
|
918
|
+
this.makerAddress = makerAddress;
|
|
919
|
+
this.feeRateBps = feeRateBps;
|
|
920
|
+
this.priceTick = priceTick;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Builds an unsigned order payload.
|
|
924
|
+
*
|
|
925
|
+
* @param args - Order arguments (FOK or GTC)
|
|
926
|
+
* @returns Unsigned order ready for signing
|
|
927
|
+
*
|
|
928
|
+
* @throws Error if validation fails or tick alignment fails
|
|
929
|
+
*
|
|
930
|
+
* @example
|
|
931
|
+
* ```typescript
|
|
932
|
+
* // FOK order (market order)
|
|
933
|
+
* const fokOrder = builder.buildOrder({
|
|
934
|
+
* tokenId: '123456',
|
|
935
|
+
* makerAmount: 50, // 50 USDC to spend
|
|
936
|
+
* side: Side.BUY,
|
|
937
|
+
* marketType: MarketType.CLOB
|
|
938
|
+
* });
|
|
939
|
+
*
|
|
940
|
+
* // GTC order (price + size)
|
|
941
|
+
* const gtcOrder = builder.buildOrder({
|
|
942
|
+
* tokenId: '123456',
|
|
943
|
+
* price: 0.38,
|
|
944
|
+
* size: 22.123, // Will be rounded to tick-aligned: 22.123 shares
|
|
945
|
+
* side: Side.BUY,
|
|
946
|
+
* marketType: MarketType.CLOB
|
|
947
|
+
* });
|
|
948
|
+
* ```
|
|
949
|
+
*/
|
|
950
|
+
buildOrder(args) {
|
|
951
|
+
this.validateOrderArgs(args);
|
|
952
|
+
const { makerAmount, takerAmount, price } = this.isFOKOrder(args) ? this.calculateFOKAmounts(args.makerAmount) : this.calculateGTCAmountsTickAligned(args.price, args.size, args.side);
|
|
953
|
+
const order = {
|
|
954
|
+
salt: this.generateSalt(),
|
|
955
|
+
maker: this.makerAddress,
|
|
956
|
+
signer: this.makerAddress,
|
|
957
|
+
taker: args.taker || ZERO_ADDRESS2,
|
|
958
|
+
tokenId: args.tokenId,
|
|
959
|
+
makerAmount,
|
|
960
|
+
takerAmount,
|
|
961
|
+
expiration: args.expiration || "0",
|
|
962
|
+
nonce: args.nonce || 0,
|
|
963
|
+
feeRateBps: this.feeRateBps,
|
|
964
|
+
side: args.side,
|
|
965
|
+
signatureType: 0 /* EOA */
|
|
966
|
+
};
|
|
967
|
+
if (price !== void 0) {
|
|
968
|
+
order.price = price;
|
|
969
|
+
}
|
|
970
|
+
return order;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Type guard to check if order arguments are for FOK order.
|
|
974
|
+
*
|
|
975
|
+
* @param args - Order arguments
|
|
976
|
+
* @returns True if FOK order arguments
|
|
977
|
+
*
|
|
978
|
+
* @internal
|
|
979
|
+
*/
|
|
980
|
+
isFOKOrder(args) {
|
|
981
|
+
return "amount" in args;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Generates a unique salt using timestamp + nano-offset pattern.
|
|
985
|
+
*
|
|
986
|
+
* @remarks
|
|
987
|
+
* This follows the reference implementation pattern:
|
|
988
|
+
* salt = timestamp * 1000 + nanoOffset + 24h
|
|
989
|
+
*
|
|
990
|
+
* This ensures uniqueness even when creating orders rapidly.
|
|
991
|
+
*
|
|
992
|
+
* @returns Unique salt value
|
|
993
|
+
*
|
|
994
|
+
* @internal
|
|
995
|
+
*/
|
|
996
|
+
generateSalt() {
|
|
997
|
+
const timestamp = Date.now();
|
|
998
|
+
const nanoOffset = Math.floor(performance.now() * 1e3) % 1e6;
|
|
999
|
+
const oneDayMs = 1e3 * 60 * 60 * 24;
|
|
1000
|
+
return timestamp * 1e3 + nanoOffset + oneDayMs;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Parses decimal string to scaled BigInt.
|
|
1004
|
+
*
|
|
1005
|
+
* @param value - Decimal string (e.g., "0.38")
|
|
1006
|
+
* @param scale - Scale factor (e.g., 1_000_000n for 6 decimals)
|
|
1007
|
+
* @returns Scaled BigInt value
|
|
1008
|
+
*
|
|
1009
|
+
* @internal
|
|
1010
|
+
*/
|
|
1011
|
+
parseDecToInt(value, scale) {
|
|
1012
|
+
const s = value.trim();
|
|
1013
|
+
const [intPart, fracPart = ""] = s.split(".");
|
|
1014
|
+
const decimals = scale.toString().length - 1;
|
|
1015
|
+
const frac = (fracPart + "0".repeat(decimals)).slice(0, decimals);
|
|
1016
|
+
const sign = intPart.startsWith("-") ? -1n : 1n;
|
|
1017
|
+
const intPartAbs = intPart.replace("-", "");
|
|
1018
|
+
return sign * (BigInt(intPartAbs || "0") * scale + BigInt(frac || "0"));
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Ceiling division for BigInt.
|
|
1022
|
+
*
|
|
1023
|
+
* @param numerator - Numerator
|
|
1024
|
+
* @param denominator - Denominator
|
|
1025
|
+
* @returns Ceiling of numerator / denominator
|
|
1026
|
+
*
|
|
1027
|
+
* @internal
|
|
1028
|
+
*/
|
|
1029
|
+
divCeil(numerator, denominator) {
|
|
1030
|
+
if (denominator === 0n) {
|
|
1031
|
+
throw new Error("Division by zero");
|
|
1032
|
+
}
|
|
1033
|
+
return (numerator + denominator - 1n) / denominator;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Calculates maker and taker amounts for GTC orders with tick alignment validation.
|
|
1037
|
+
*
|
|
1038
|
+
* @remarks
|
|
1039
|
+
* Validates and calculates amounts to ensure:
|
|
1040
|
+
* 1. Price aligns to tick size (e.g., 0.001 for 3 decimals)
|
|
1041
|
+
* 2. Size produces takerAmount divisible by sharesStep
|
|
1042
|
+
* 3. No auto-rounding - fails fast if values are invalid
|
|
1043
|
+
* 4. Transparent error messages guide users to valid values
|
|
1044
|
+
*
|
|
1045
|
+
* **Algorithm**:
|
|
1046
|
+
* - sharesStep = priceScale / tickInt (e.g., 1000 for 0.001 tick)
|
|
1047
|
+
* - Validates shares are divisible by sharesStep
|
|
1048
|
+
* - Calculates collateral from shares × price (ceil for BUY, floor for SELL)
|
|
1049
|
+
* - Assigns maker/taker based on side:
|
|
1050
|
+
* - BUY: maker = collateral, taker = shares
|
|
1051
|
+
* - SELL: maker = shares, taker = collateral
|
|
1052
|
+
* - Throws clear error if size is not tick-aligned
|
|
1053
|
+
*
|
|
1054
|
+
* @param price - Price per share (0.0 to 1.0, max 3 decimals)
|
|
1055
|
+
* @param size - Number of shares (must be tick-aligned)
|
|
1056
|
+
* @param side - Order side (BUY or SELL)
|
|
1057
|
+
* @returns Object with validated makerAmount, takerAmount, and price
|
|
1058
|
+
*
|
|
1059
|
+
* @throws Error if price or size not tick-aligned
|
|
1060
|
+
*
|
|
1061
|
+
* @internal
|
|
1062
|
+
*/
|
|
1063
|
+
calculateGTCAmountsTickAligned(price, size, side) {
|
|
1064
|
+
const sharesScale = 1000000n;
|
|
1065
|
+
const collateralScale = 1000000n;
|
|
1066
|
+
const priceScale = 1000000n;
|
|
1067
|
+
const shares = this.parseDecToInt(size.toString(), sharesScale);
|
|
1068
|
+
const priceInt = this.parseDecToInt(price.toString(), priceScale);
|
|
1069
|
+
const tickInt = this.parseDecToInt(this.priceTick.toString(), priceScale);
|
|
1070
|
+
if (tickInt <= 0n) {
|
|
1071
|
+
throw new Error(`Invalid priceTick: ${this.priceTick}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (priceInt <= 0n) {
|
|
1074
|
+
throw new Error(`Invalid price: ${price}`);
|
|
1075
|
+
}
|
|
1076
|
+
if (priceInt % tickInt !== 0n) {
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`Price ${price} is not tick-aligned. Must be multiple of ${this.priceTick} (e.g., 0.380, 0.381, etc.)`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
const sharesStep = priceScale / tickInt;
|
|
1082
|
+
if (shares % sharesStep !== 0n) {
|
|
1083
|
+
const validSizeDown = Number(shares / sharesStep * sharesStep) / 1e6;
|
|
1084
|
+
const validSizeUp = Number(this.divCeil(shares, sharesStep) * sharesStep) / 1e6;
|
|
1085
|
+
throw new Error(
|
|
1086
|
+
`Invalid size: ${size}. Size must produce contracts divisible by ${sharesStep} (sharesStep). Try ${validSizeDown} (rounded down) or ${validSizeUp} (rounded up) instead.`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
const numerator = shares * priceInt * collateralScale;
|
|
1090
|
+
const denominator = sharesScale * priceScale;
|
|
1091
|
+
const collateral = side === 0 /* BUY */ ? this.divCeil(numerator, denominator) : numerator / denominator;
|
|
1092
|
+
let makerAmount;
|
|
1093
|
+
let takerAmount;
|
|
1094
|
+
if (side === 0 /* BUY */) {
|
|
1095
|
+
makerAmount = collateral;
|
|
1096
|
+
takerAmount = shares;
|
|
1097
|
+
} else {
|
|
1098
|
+
makerAmount = shares;
|
|
1099
|
+
takerAmount = collateral;
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
makerAmount: Number(makerAmount),
|
|
1103
|
+
takerAmount: Number(takerAmount),
|
|
1104
|
+
price
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Calculates maker and taker amounts for FOK (market) orders.
|
|
1109
|
+
*
|
|
1110
|
+
* @remarks
|
|
1111
|
+
* FOK orders use makerAmount for both BUY and SELL:
|
|
1112
|
+
* - BUY: makerAmount = USDC amount to spend (e.g., 50 = $50 USDC)
|
|
1113
|
+
* - SELL: makerAmount = number of shares to sell (e.g., 18.64 shares)
|
|
1114
|
+
*
|
|
1115
|
+
* takerAmount is always 1 (constant for FOK orders)
|
|
1116
|
+
*
|
|
1117
|
+
* @param makerAmount - Amount in human-readable format (max 6 decimals)
|
|
1118
|
+
* @returns Object with makerAmount (scaled), takerAmount (always 1), and undefined price
|
|
1119
|
+
*
|
|
1120
|
+
* @internal
|
|
1121
|
+
*/
|
|
1122
|
+
calculateFOKAmounts(makerAmount) {
|
|
1123
|
+
const DECIMALS = 6;
|
|
1124
|
+
const amountStr = makerAmount.toString();
|
|
1125
|
+
const decimalIndex = amountStr.indexOf(".");
|
|
1126
|
+
if (decimalIndex !== -1) {
|
|
1127
|
+
const decimalPlaces = amountStr.length - decimalIndex - 1;
|
|
1128
|
+
if (decimalPlaces > DECIMALS) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`Invalid makerAmount: ${makerAmount}. Can have max ${DECIMALS} decimal places. Try ${makerAmount.toFixed(DECIMALS)} instead.`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const amountFormatted = makerAmount.toFixed(DECIMALS);
|
|
1135
|
+
const amountScaled = import_ethers2.ethers.parseUnits(amountFormatted, DECIMALS);
|
|
1136
|
+
const collateralAmount = Number(amountScaled);
|
|
1137
|
+
return {
|
|
1138
|
+
makerAmount: collateralAmount,
|
|
1139
|
+
takerAmount: 1,
|
|
1140
|
+
price: void 0
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Validates order arguments.
|
|
1145
|
+
*
|
|
1146
|
+
* @param args - Order arguments to validate
|
|
1147
|
+
* @throws Error if validation fails
|
|
1148
|
+
*
|
|
1149
|
+
* @internal
|
|
1150
|
+
*/
|
|
1151
|
+
validateOrderArgs(args) {
|
|
1152
|
+
if (!args.tokenId || args.tokenId === "0") {
|
|
1153
|
+
throw new Error("Invalid tokenId: tokenId is required.");
|
|
1154
|
+
}
|
|
1155
|
+
if (args.taker && !import_ethers2.ethers.isAddress(args.taker)) {
|
|
1156
|
+
throw new Error(`Invalid taker address: ${args.taker}`);
|
|
1157
|
+
}
|
|
1158
|
+
if (this.isFOKOrder(args)) {
|
|
1159
|
+
if (!args.makerAmount) {
|
|
1160
|
+
throw new Error("FOK orders require makerAmount");
|
|
1161
|
+
}
|
|
1162
|
+
if (args.makerAmount <= 0) {
|
|
1163
|
+
throw new Error(`Invalid makerAmount: ${args.makerAmount}. Maker amount must be positive.`);
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
if (args.price < 0 || args.price > 1) {
|
|
1167
|
+
throw new Error(`Invalid price: ${args.price}. Price must be between 0 and 1.`);
|
|
1168
|
+
}
|
|
1169
|
+
if (args.size <= 0) {
|
|
1170
|
+
throw new Error(`Invalid size: ${args.size}. Size must be positive.`);
|
|
1171
|
+
}
|
|
1172
|
+
const priceStr = args.price.toString();
|
|
1173
|
+
const decimalIndex = priceStr.indexOf(".");
|
|
1174
|
+
if (decimalIndex !== -1) {
|
|
1175
|
+
const decimalPlaces = priceStr.length - decimalIndex - 1;
|
|
1176
|
+
if (decimalPlaces > 3) {
|
|
1177
|
+
throw new Error(
|
|
1178
|
+
`Invalid price: ${args.price}. Price must have max 3 decimal places (e.g., 0.380, 0.001).`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/orders/signer.ts
|
|
1187
|
+
var OrderSigner = class {
|
|
1188
|
+
/**
|
|
1189
|
+
* Creates a new order signer instance.
|
|
1190
|
+
*
|
|
1191
|
+
* @param wallet - Ethers wallet for signing
|
|
1192
|
+
* @param logger - Optional logger for debugging (default: no logging)
|
|
1193
|
+
*
|
|
1194
|
+
* @example
|
|
1195
|
+
* ```typescript
|
|
1196
|
+
* import { ethers } from 'ethers';
|
|
1197
|
+
* import { OrderSigner } from '@limitless-exchange/sdk';
|
|
1198
|
+
*
|
|
1199
|
+
* const wallet = new ethers.Wallet(privateKey);
|
|
1200
|
+
* const signer = new OrderSigner(wallet);
|
|
1201
|
+
* ```
|
|
1202
|
+
*/
|
|
1203
|
+
constructor(wallet, logger) {
|
|
1204
|
+
this.wallet = wallet;
|
|
1205
|
+
this.logger = logger || new NoOpLogger();
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Signs an order with EIP-712.
|
|
1209
|
+
*
|
|
1210
|
+
* @param order - Unsigned order to sign
|
|
1211
|
+
* @param config - Signing configuration (chainId, contract address, market type)
|
|
1212
|
+
* @returns Promise resolving to EIP-712 signature
|
|
1213
|
+
*
|
|
1214
|
+
* @throws Error if wallet address doesn't match order signer
|
|
1215
|
+
* @throws Error if signing fails
|
|
1216
|
+
*
|
|
1217
|
+
* @example
|
|
1218
|
+
* ```typescript
|
|
1219
|
+
* const signature = await signer.signOrder(unsignedOrder, {
|
|
1220
|
+
* chainId: 8453,
|
|
1221
|
+
* contractAddress: '0x...',
|
|
1222
|
+
* marketType: MarketType.CLOB
|
|
1223
|
+
* });
|
|
1224
|
+
* ```
|
|
1225
|
+
*/
|
|
1226
|
+
async signOrder(order, config) {
|
|
1227
|
+
this.logger.debug("Signing order with EIP-712", {
|
|
1228
|
+
tokenId: order.tokenId,
|
|
1229
|
+
side: order.side,
|
|
1230
|
+
marketType: config.marketType
|
|
1231
|
+
});
|
|
1232
|
+
const walletAddress = await this.wallet.getAddress();
|
|
1233
|
+
if (walletAddress.toLowerCase() !== order.signer.toLowerCase()) {
|
|
1234
|
+
const error = `Wallet address mismatch! Signing with: ${walletAddress}, but order signer is: ${order.signer}`;
|
|
1235
|
+
this.logger.error(error);
|
|
1236
|
+
throw new Error(error);
|
|
1237
|
+
}
|
|
1238
|
+
const domain = this.getDomain(config);
|
|
1239
|
+
this.logger.debug("EIP-712 Domain", domain);
|
|
1240
|
+
const types = this.getTypes();
|
|
1241
|
+
const orderValue = {
|
|
1242
|
+
salt: order.salt,
|
|
1243
|
+
maker: order.maker,
|
|
1244
|
+
signer: order.signer,
|
|
1245
|
+
taker: order.taker,
|
|
1246
|
+
tokenId: order.tokenId,
|
|
1247
|
+
makerAmount: order.makerAmount,
|
|
1248
|
+
takerAmount: order.takerAmount,
|
|
1249
|
+
expiration: order.expiration,
|
|
1250
|
+
nonce: order.nonce,
|
|
1251
|
+
feeRateBps: order.feeRateBps,
|
|
1252
|
+
side: order.side,
|
|
1253
|
+
signatureType: order.signatureType
|
|
1254
|
+
};
|
|
1255
|
+
this.logger.debug("EIP-712 Order Value", orderValue);
|
|
1256
|
+
console.log("[OrderSigner] Full signing payload:", JSON.stringify({
|
|
1257
|
+
domain,
|
|
1258
|
+
types: this.getTypes(),
|
|
1259
|
+
value: orderValue
|
|
1260
|
+
}, null, 2));
|
|
1261
|
+
try {
|
|
1262
|
+
const signature = await this.wallet.signTypedData(domain, types, orderValue);
|
|
1263
|
+
this.logger.info("Successfully generated EIP-712 signature", {
|
|
1264
|
+
signature: signature.slice(0, 10) + "..."
|
|
1265
|
+
});
|
|
1266
|
+
return signature;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
this.logger.error("Failed to sign order", error);
|
|
1269
|
+
throw error;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Gets the EIP-712 domain for signing.
|
|
1274
|
+
*
|
|
1275
|
+
* @param config - Signing configuration
|
|
1276
|
+
* @returns EIP-712 domain object
|
|
1277
|
+
*
|
|
1278
|
+
* @internal
|
|
1279
|
+
*/
|
|
1280
|
+
getDomain(config) {
|
|
1281
|
+
return {
|
|
1282
|
+
name: "Limitless CTF Exchange",
|
|
1283
|
+
version: "1",
|
|
1284
|
+
chainId: config.chainId,
|
|
1285
|
+
verifyingContract: config.contractAddress
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Gets the EIP-712 type definitions.
|
|
1290
|
+
*
|
|
1291
|
+
* @remarks
|
|
1292
|
+
* This matches the order structure expected by the Limitless Exchange
|
|
1293
|
+
* smart contracts.
|
|
1294
|
+
*
|
|
1295
|
+
* @returns EIP-712 types definition
|
|
1296
|
+
*
|
|
1297
|
+
* @internal
|
|
1298
|
+
*/
|
|
1299
|
+
getTypes() {
|
|
1300
|
+
return {
|
|
1301
|
+
Order: [
|
|
1302
|
+
{ name: "salt", type: "uint256" },
|
|
1303
|
+
{ name: "maker", type: "address" },
|
|
1304
|
+
{ name: "signer", type: "address" },
|
|
1305
|
+
{ name: "taker", type: "address" },
|
|
1306
|
+
{ name: "tokenId", type: "uint256" },
|
|
1307
|
+
{ name: "makerAmount", type: "uint256" },
|
|
1308
|
+
{ name: "takerAmount", type: "uint256" },
|
|
1309
|
+
{ name: "expiration", type: "uint256" },
|
|
1310
|
+
{ name: "nonce", type: "uint256" },
|
|
1311
|
+
{ name: "feeRateBps", type: "uint256" },
|
|
1312
|
+
{ name: "side", type: "uint8" },
|
|
1313
|
+
{ name: "signatureType", type: "uint8" }
|
|
1314
|
+
]
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
// src/orders/validator.ts
|
|
1320
|
+
var import_ethers3 = require("ethers");
|
|
1321
|
+
var ValidationError = class extends Error {
|
|
1322
|
+
constructor(message) {
|
|
1323
|
+
super(message);
|
|
1324
|
+
this.name = "ValidationError";
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
function isFOKOrder(args) {
|
|
1328
|
+
return "amount" in args;
|
|
1329
|
+
}
|
|
1330
|
+
function validateOrderArgs(args) {
|
|
1331
|
+
if (!args.tokenId) {
|
|
1332
|
+
throw new ValidationError("TokenId is required");
|
|
1333
|
+
}
|
|
1334
|
+
if (args.tokenId === "0") {
|
|
1335
|
+
throw new ValidationError("TokenId cannot be zero");
|
|
1336
|
+
}
|
|
1337
|
+
if (!/^\d+$/.test(args.tokenId)) {
|
|
1338
|
+
throw new ValidationError(`Invalid tokenId format: ${args.tokenId}`);
|
|
1339
|
+
}
|
|
1340
|
+
if (args.taker && !import_ethers3.ethers.isAddress(args.taker)) {
|
|
1341
|
+
throw new ValidationError(`Invalid taker address: ${args.taker}`);
|
|
1342
|
+
}
|
|
1343
|
+
if (args.expiration !== void 0) {
|
|
1344
|
+
if (!/^\d+$/.test(args.expiration)) {
|
|
1345
|
+
throw new ValidationError(`Invalid expiration format: ${args.expiration}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (args.nonce !== void 0) {
|
|
1349
|
+
if (!Number.isInteger(args.nonce) || args.nonce < 0) {
|
|
1350
|
+
throw new ValidationError(`Invalid nonce: ${args.nonce}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (isFOKOrder(args)) {
|
|
1354
|
+
if (typeof args.makerAmount !== "number" || isNaN(args.makerAmount)) {
|
|
1355
|
+
throw new ValidationError("Amount must be a valid number");
|
|
1356
|
+
}
|
|
1357
|
+
if (args.makerAmount <= 0) {
|
|
1358
|
+
throw new ValidationError(`Amount must be positive, got: ${args.makerAmount}`);
|
|
1359
|
+
}
|
|
1360
|
+
const amountStr = args.makerAmount.toString();
|
|
1361
|
+
const decimalIndex = amountStr.indexOf(".");
|
|
1362
|
+
if (decimalIndex !== -1) {
|
|
1363
|
+
const decimalPlaces = amountStr.length - decimalIndex - 1;
|
|
1364
|
+
if (decimalPlaces > 2) {
|
|
1365
|
+
throw new ValidationError(
|
|
1366
|
+
`Amount must have max 2 decimal places, got: ${args.makerAmount} (${decimalPlaces} decimals)`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
if (typeof args.price !== "number" || isNaN(args.price)) {
|
|
1372
|
+
throw new ValidationError("Price must be a valid number");
|
|
1373
|
+
}
|
|
1374
|
+
if (args.price < 0 || args.price > 1) {
|
|
1375
|
+
throw new ValidationError(`Price must be between 0 and 1, got: ${args.price}`);
|
|
1376
|
+
}
|
|
1377
|
+
if (typeof args.size !== "number" || isNaN(args.size)) {
|
|
1378
|
+
throw new ValidationError("Size must be a valid number");
|
|
1379
|
+
}
|
|
1380
|
+
if (args.size <= 0) {
|
|
1381
|
+
throw new ValidationError(`Size must be positive, got: ${args.size}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function validateUnsignedOrder(order) {
|
|
1386
|
+
if (!import_ethers3.ethers.isAddress(order.maker)) {
|
|
1387
|
+
throw new ValidationError(`Invalid maker address: ${order.maker}`);
|
|
1388
|
+
}
|
|
1389
|
+
if (!import_ethers3.ethers.isAddress(order.signer)) {
|
|
1390
|
+
throw new ValidationError(`Invalid signer address: ${order.signer}`);
|
|
1391
|
+
}
|
|
1392
|
+
if (!import_ethers3.ethers.isAddress(order.taker)) {
|
|
1393
|
+
throw new ValidationError(`Invalid taker address: ${order.taker}`);
|
|
1394
|
+
}
|
|
1395
|
+
if (!order.makerAmount || order.makerAmount === 0) {
|
|
1396
|
+
throw new ValidationError("MakerAmount must be greater than zero");
|
|
1397
|
+
}
|
|
1398
|
+
if (!order.takerAmount || order.takerAmount === 0) {
|
|
1399
|
+
throw new ValidationError("TakerAmount must be greater than zero");
|
|
1400
|
+
}
|
|
1401
|
+
if (typeof order.makerAmount !== "number" || order.makerAmount <= 0) {
|
|
1402
|
+
throw new ValidationError(`Invalid makerAmount: ${order.makerAmount}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (typeof order.takerAmount !== "number" || order.takerAmount <= 0) {
|
|
1405
|
+
throw new ValidationError(`Invalid takerAmount: ${order.takerAmount}`);
|
|
1406
|
+
}
|
|
1407
|
+
if (!/^\d+$/.test(order.tokenId)) {
|
|
1408
|
+
throw new ValidationError(`Invalid tokenId format: ${order.tokenId}`);
|
|
1409
|
+
}
|
|
1410
|
+
if (!/^\d+$/.test(order.expiration)) {
|
|
1411
|
+
throw new ValidationError(`Invalid expiration format: ${order.expiration}`);
|
|
1412
|
+
}
|
|
1413
|
+
if (!Number.isInteger(order.salt) || order.salt <= 0) {
|
|
1414
|
+
throw new ValidationError(`Invalid salt: ${order.salt}`);
|
|
1415
|
+
}
|
|
1416
|
+
if (!Number.isInteger(order.nonce) || order.nonce < 0) {
|
|
1417
|
+
throw new ValidationError(`Invalid nonce: ${order.nonce}`);
|
|
1418
|
+
}
|
|
1419
|
+
if (!Number.isInteger(order.feeRateBps) || order.feeRateBps < 0) {
|
|
1420
|
+
throw new ValidationError(`Invalid feeRateBps: ${order.feeRateBps}`);
|
|
1421
|
+
}
|
|
1422
|
+
if (order.side !== 0 && order.side !== 1) {
|
|
1423
|
+
throw new ValidationError(`Invalid side: ${order.side}. Must be 0 (BUY) or 1 (SELL)`);
|
|
1424
|
+
}
|
|
1425
|
+
if (!Number.isInteger(order.signatureType) || order.signatureType < 0) {
|
|
1426
|
+
throw new ValidationError(`Invalid signatureType: ${order.signatureType}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (order.price !== void 0) {
|
|
1429
|
+
if (typeof order.price !== "number" || isNaN(order.price)) {
|
|
1430
|
+
throw new ValidationError("Price must be a valid number");
|
|
1431
|
+
}
|
|
1432
|
+
if (order.price < 0 || order.price > 1) {
|
|
1433
|
+
throw new ValidationError(`Price must be between 0 and 1, got: ${order.price}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function validateSignedOrder(order) {
|
|
1438
|
+
validateUnsignedOrder(order);
|
|
1439
|
+
if (!order.signature) {
|
|
1440
|
+
throw new ValidationError("Signature is required");
|
|
1441
|
+
}
|
|
1442
|
+
if (!order.signature.startsWith("0x")) {
|
|
1443
|
+
throw new ValidationError("Signature must start with 0x");
|
|
1444
|
+
}
|
|
1445
|
+
if (order.signature.length !== 132) {
|
|
1446
|
+
throw new ValidationError(
|
|
1447
|
+
`Invalid signature length: ${order.signature.length}. Expected 132 characters.`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(order.signature)) {
|
|
1451
|
+
throw new ValidationError("Signature must be valid hex string");
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/orders/client.ts
|
|
1456
|
+
var OrderClient = class {
|
|
1457
|
+
/**
|
|
1458
|
+
* Creates a new order client instance.
|
|
1459
|
+
*
|
|
1460
|
+
* @param config - Order client configuration
|
|
1461
|
+
*
|
|
1462
|
+
* @throws Error if neither marketType nor signingConfig is provided
|
|
1463
|
+
*/
|
|
1464
|
+
constructor(config) {
|
|
1465
|
+
this.httpClient = config.httpClient;
|
|
1466
|
+
this.logger = config.logger || new NoOpLogger();
|
|
1467
|
+
this.ownerId = config.userData.userId;
|
|
1468
|
+
const feeRateBps = config.userData.feeRateBps;
|
|
1469
|
+
this.orderBuilder = new OrderBuilder(config.wallet.address, feeRateBps, 1e-3);
|
|
1470
|
+
this.orderSigner = new OrderSigner(config.wallet, this.logger);
|
|
1471
|
+
if (config.signingConfig) {
|
|
1472
|
+
this.signingConfig = config.signingConfig;
|
|
1473
|
+
} else if (config.marketType) {
|
|
1474
|
+
const chainId = parseInt(process.env.CHAIN_ID || "8453");
|
|
1475
|
+
const contractAddress = config.marketType === "NEGRISK" /* NEGRISK */ ? process.env.NEGRISK_CONTRACT_ADDRESS || getContractAddress("NEGRISK", chainId) : process.env.CLOB_CONTRACT_ADDRESS || getContractAddress("CLOB", chainId);
|
|
1476
|
+
this.signingConfig = {
|
|
1477
|
+
chainId,
|
|
1478
|
+
contractAddress,
|
|
1479
|
+
marketType: config.marketType
|
|
1480
|
+
};
|
|
1481
|
+
this.logger.info("Auto-configured signing", {
|
|
1482
|
+
chainId,
|
|
1483
|
+
contractAddress,
|
|
1484
|
+
marketType: config.marketType
|
|
1485
|
+
});
|
|
1486
|
+
} else {
|
|
1487
|
+
const chainId = parseInt(process.env.CHAIN_ID || "8453");
|
|
1488
|
+
const contractAddress = process.env.CLOB_CONTRACT_ADDRESS || getContractAddress("CLOB", chainId);
|
|
1489
|
+
this.signingConfig = {
|
|
1490
|
+
chainId,
|
|
1491
|
+
contractAddress,
|
|
1492
|
+
marketType: "CLOB" /* CLOB */
|
|
1493
|
+
};
|
|
1494
|
+
this.logger.debug("Using default CLOB configuration", {
|
|
1495
|
+
chainId,
|
|
1496
|
+
contractAddress
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Creates and submits a new order.
|
|
1502
|
+
*
|
|
1503
|
+
* @remarks
|
|
1504
|
+
* This method handles the complete order creation flow:
|
|
1505
|
+
* 1. Build unsigned order
|
|
1506
|
+
* 2. Sign with EIP-712
|
|
1507
|
+
* 3. Submit to API
|
|
1508
|
+
*
|
|
1509
|
+
* @param params - Order parameters
|
|
1510
|
+
* @returns Promise resolving to order response
|
|
1511
|
+
*
|
|
1512
|
+
* @throws Error if order creation fails
|
|
1513
|
+
*
|
|
1514
|
+
* @example
|
|
1515
|
+
* ```typescript
|
|
1516
|
+
* const order = await orderClient.createOrder({
|
|
1517
|
+
* tokenId: '123456',
|
|
1518
|
+
* price: 0.65,
|
|
1519
|
+
* size: 100,
|
|
1520
|
+
* side: Side.BUY,
|
|
1521
|
+
* orderType: OrderType.GTC,
|
|
1522
|
+
* marketSlug: 'market-slug'
|
|
1523
|
+
* });
|
|
1524
|
+
*
|
|
1525
|
+
* console.log(`Order created: ${order.order.id}`);
|
|
1526
|
+
* ```
|
|
1527
|
+
*/
|
|
1528
|
+
async createOrder(params) {
|
|
1529
|
+
this.logger.info("Creating order", {
|
|
1530
|
+
side: params.side,
|
|
1531
|
+
orderType: params.orderType,
|
|
1532
|
+
marketSlug: params.marketSlug
|
|
1533
|
+
});
|
|
1534
|
+
const unsignedOrder = this.orderBuilder.buildOrder(params);
|
|
1535
|
+
this.logger.debug("Built unsigned order", {
|
|
1536
|
+
salt: unsignedOrder.salt,
|
|
1537
|
+
makerAmount: unsignedOrder.makerAmount,
|
|
1538
|
+
takerAmount: unsignedOrder.takerAmount
|
|
1539
|
+
});
|
|
1540
|
+
const signature = await this.orderSigner.signOrder(
|
|
1541
|
+
unsignedOrder,
|
|
1542
|
+
this.signingConfig
|
|
1543
|
+
);
|
|
1544
|
+
const payload = {
|
|
1545
|
+
order: {
|
|
1546
|
+
...unsignedOrder,
|
|
1547
|
+
signature
|
|
1548
|
+
},
|
|
1549
|
+
orderType: params.orderType,
|
|
1550
|
+
marketSlug: params.marketSlug,
|
|
1551
|
+
ownerId: this.ownerId
|
|
1552
|
+
};
|
|
1553
|
+
this.logger.debug("Submitting order to API");
|
|
1554
|
+
console.log("[OrderClient] Full API request payload:", JSON.stringify(payload, null, 2));
|
|
1555
|
+
const apiResponse = await this.httpClient.post(
|
|
1556
|
+
"/orders",
|
|
1557
|
+
payload
|
|
1558
|
+
);
|
|
1559
|
+
this.logger.info("Order created successfully", {
|
|
1560
|
+
orderId: apiResponse.order.id
|
|
1561
|
+
});
|
|
1562
|
+
return this.transformOrderResponse(apiResponse);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Transforms raw API response to clean OrderResponse DTO.
|
|
1566
|
+
*
|
|
1567
|
+
* @param apiResponse - Raw API response with nested objects
|
|
1568
|
+
* @returns Clean OrderResponse with only essential fields
|
|
1569
|
+
*
|
|
1570
|
+
* @internal
|
|
1571
|
+
*/
|
|
1572
|
+
transformOrderResponse(apiResponse) {
|
|
1573
|
+
const cleanOrder = {
|
|
1574
|
+
order: {
|
|
1575
|
+
id: apiResponse.order.id,
|
|
1576
|
+
createdAt: apiResponse.order.createdAt,
|
|
1577
|
+
makerAmount: apiResponse.order.makerAmount,
|
|
1578
|
+
takerAmount: apiResponse.order.takerAmount,
|
|
1579
|
+
expiration: apiResponse.order.expiration,
|
|
1580
|
+
signatureType: apiResponse.order.signatureType,
|
|
1581
|
+
salt: apiResponse.order.salt,
|
|
1582
|
+
maker: apiResponse.order.maker,
|
|
1583
|
+
signer: apiResponse.order.signer,
|
|
1584
|
+
taker: apiResponse.order.taker,
|
|
1585
|
+
tokenId: apiResponse.order.tokenId,
|
|
1586
|
+
side: apiResponse.order.side,
|
|
1587
|
+
feeRateBps: apiResponse.order.feeRateBps,
|
|
1588
|
+
nonce: apiResponse.order.nonce,
|
|
1589
|
+
signature: apiResponse.order.signature,
|
|
1590
|
+
orderType: apiResponse.order.orderType,
|
|
1591
|
+
price: apiResponse.order.price,
|
|
1592
|
+
marketId: apiResponse.order.marketId
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
if (apiResponse.makerMatches && apiResponse.makerMatches.length > 0) {
|
|
1596
|
+
cleanOrder.makerMatches = apiResponse.makerMatches.map((match) => ({
|
|
1597
|
+
id: match.id,
|
|
1598
|
+
createdAt: match.createdAt,
|
|
1599
|
+
matchedSize: match.matchedSize,
|
|
1600
|
+
orderId: match.orderId
|
|
1601
|
+
}));
|
|
1602
|
+
}
|
|
1603
|
+
return cleanOrder;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Cancels an existing order by ID.
|
|
1607
|
+
*
|
|
1608
|
+
* @param orderId - Order ID to cancel
|
|
1609
|
+
* @returns Promise resolving to cancellation message
|
|
1610
|
+
*
|
|
1611
|
+
* @throws Error if cancellation fails
|
|
1612
|
+
*
|
|
1613
|
+
* @example
|
|
1614
|
+
* ```typescript
|
|
1615
|
+
* const result = await orderClient.cancel('order-id-123');
|
|
1616
|
+
* console.log(result.message); // "Order canceled successfully"
|
|
1617
|
+
* ```
|
|
1618
|
+
*/
|
|
1619
|
+
async cancel(orderId) {
|
|
1620
|
+
this.logger.info("Cancelling order", { orderId });
|
|
1621
|
+
const response = await this.httpClient.delete(
|
|
1622
|
+
`/orders/${orderId}`
|
|
1623
|
+
);
|
|
1624
|
+
this.logger.info("Order cancellation response", {
|
|
1625
|
+
orderId,
|
|
1626
|
+
message: response.message
|
|
1627
|
+
});
|
|
1628
|
+
return response;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Cancels all orders for a specific market.
|
|
1632
|
+
*
|
|
1633
|
+
* @param marketSlug - Market slug to cancel all orders for
|
|
1634
|
+
* @returns Promise resolving to cancellation message
|
|
1635
|
+
*
|
|
1636
|
+
* @throws Error if cancellation fails
|
|
1637
|
+
*
|
|
1638
|
+
* @example
|
|
1639
|
+
* ```typescript
|
|
1640
|
+
* const result = await orderClient.cancelAll('market-slug-123');
|
|
1641
|
+
* console.log(result.message); // "Orders canceled successfully"
|
|
1642
|
+
* ```
|
|
1643
|
+
*/
|
|
1644
|
+
async cancelAll(marketSlug) {
|
|
1645
|
+
this.logger.info("Cancelling all orders for market", { marketSlug });
|
|
1646
|
+
const response = await this.httpClient.delete(
|
|
1647
|
+
`/orders/all/${marketSlug}`
|
|
1648
|
+
);
|
|
1649
|
+
this.logger.info("All orders cancellation response", {
|
|
1650
|
+
marketSlug,
|
|
1651
|
+
message: response.message
|
|
1652
|
+
});
|
|
1653
|
+
return response;
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* @deprecated Use `cancel()` instead
|
|
1657
|
+
*/
|
|
1658
|
+
async cancelOrder(orderId) {
|
|
1659
|
+
await this.cancel(orderId);
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Gets an order by ID.
|
|
1663
|
+
*
|
|
1664
|
+
* @param orderId - Order ID to fetch
|
|
1665
|
+
* @returns Promise resolving to order details
|
|
1666
|
+
*
|
|
1667
|
+
* @throws Error if order not found
|
|
1668
|
+
*
|
|
1669
|
+
* @example
|
|
1670
|
+
* ```typescript
|
|
1671
|
+
* const order = await orderClient.getOrder('order-id-123');
|
|
1672
|
+
* console.log(order.order.side);
|
|
1673
|
+
* ```
|
|
1674
|
+
*/
|
|
1675
|
+
async getOrder(orderId) {
|
|
1676
|
+
this.logger.debug("Fetching order", { orderId });
|
|
1677
|
+
const response = await this.httpClient.get(
|
|
1678
|
+
`/orders/${orderId}`
|
|
1679
|
+
);
|
|
1680
|
+
return response;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Builds an unsigned order without submitting.
|
|
1684
|
+
*
|
|
1685
|
+
* @remarks
|
|
1686
|
+
* Useful for advanced use cases where you need the unsigned order
|
|
1687
|
+
* before signing and submission.
|
|
1688
|
+
*
|
|
1689
|
+
* @param params - Order parameters
|
|
1690
|
+
* @returns Unsigned order
|
|
1691
|
+
*
|
|
1692
|
+
* @example
|
|
1693
|
+
* ```typescript
|
|
1694
|
+
* const unsignedOrder = orderClient.buildUnsignedOrder({
|
|
1695
|
+
* tokenId: '123456',
|
|
1696
|
+
* price: 0.65,
|
|
1697
|
+
* size: 100,
|
|
1698
|
+
* side: Side.BUY
|
|
1699
|
+
* });
|
|
1700
|
+
* ```
|
|
1701
|
+
*/
|
|
1702
|
+
buildUnsignedOrder(params) {
|
|
1703
|
+
return this.orderBuilder.buildOrder(params);
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Signs an unsigned order without submitting.
|
|
1707
|
+
*
|
|
1708
|
+
* @remarks
|
|
1709
|
+
* Useful for advanced use cases where you need to inspect
|
|
1710
|
+
* the signature before submission.
|
|
1711
|
+
*
|
|
1712
|
+
* @param order - Unsigned order to sign
|
|
1713
|
+
* @returns Promise resolving to signature
|
|
1714
|
+
*
|
|
1715
|
+
* @example
|
|
1716
|
+
* ```typescript
|
|
1717
|
+
* const signature = await orderClient.signOrder(unsignedOrder);
|
|
1718
|
+
* ```
|
|
1719
|
+
*/
|
|
1720
|
+
async signOrder(order) {
|
|
1721
|
+
return await this.orderSigner.signOrder(order, this.signingConfig);
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
// src/markets/fetcher.ts
|
|
1726
|
+
var MarketFetcher = class {
|
|
1727
|
+
/**
|
|
1728
|
+
* Creates a new market fetcher instance.
|
|
1729
|
+
*
|
|
1730
|
+
* @param httpClient - HTTP client for API requests
|
|
1731
|
+
* @param logger - Optional logger for debugging (default: no logging)
|
|
1732
|
+
*
|
|
1733
|
+
* @example
|
|
1734
|
+
* ```typescript
|
|
1735
|
+
* const fetcher = new MarketFetcher(httpClient);
|
|
1736
|
+
* ```
|
|
1737
|
+
*/
|
|
1738
|
+
constructor(httpClient, logger) {
|
|
1739
|
+
this.httpClient = httpClient;
|
|
1740
|
+
this.logger = logger || new NoOpLogger();
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Gets active markets with query parameters and pagination support.
|
|
1744
|
+
*
|
|
1745
|
+
* @param params - Query parameters for filtering and pagination
|
|
1746
|
+
* @returns Promise resolving to active markets response
|
|
1747
|
+
* @throws Error if API request fails
|
|
1748
|
+
*
|
|
1749
|
+
* @example
|
|
1750
|
+
* ```typescript
|
|
1751
|
+
* // Get 8 markets sorted by LP rewards
|
|
1752
|
+
* const response = await fetcher.getActiveMarkets({
|
|
1753
|
+
* limit: 8,
|
|
1754
|
+
* sortBy: 'lp_rewards'
|
|
1755
|
+
* });
|
|
1756
|
+
* console.log(`Found ${response.data.length} of ${response.totalMarketsCount} markets`);
|
|
1757
|
+
*
|
|
1758
|
+
* // Get page 2
|
|
1759
|
+
* const page2 = await fetcher.getActiveMarkets({
|
|
1760
|
+
* limit: 8,
|
|
1761
|
+
* page: 2,
|
|
1762
|
+
* sortBy: 'ending_soon'
|
|
1763
|
+
* });
|
|
1764
|
+
* ```
|
|
1765
|
+
*/
|
|
1766
|
+
async getActiveMarkets(params) {
|
|
1767
|
+
const queryParams = new URLSearchParams();
|
|
1768
|
+
if (params?.limit !== void 0) {
|
|
1769
|
+
queryParams.append("limit", params.limit.toString());
|
|
1770
|
+
}
|
|
1771
|
+
if (params?.page !== void 0) {
|
|
1772
|
+
queryParams.append("page", params.page.toString());
|
|
1773
|
+
}
|
|
1774
|
+
if (params?.sortBy) {
|
|
1775
|
+
queryParams.append("sortBy", params.sortBy);
|
|
1776
|
+
}
|
|
1777
|
+
const queryString = queryParams.toString();
|
|
1778
|
+
const endpoint = `/markets/active${queryString ? `?${queryString}` : ""}`;
|
|
1779
|
+
this.logger.debug("Fetching active markets", { params });
|
|
1780
|
+
try {
|
|
1781
|
+
const response = await this.httpClient.get(endpoint);
|
|
1782
|
+
this.logger.info("Active markets fetched successfully", {
|
|
1783
|
+
count: response.data.length,
|
|
1784
|
+
total: response.totalMarketsCount,
|
|
1785
|
+
sortBy: params?.sortBy,
|
|
1786
|
+
page: params?.page
|
|
1787
|
+
});
|
|
1788
|
+
return response;
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
this.logger.error("Failed to fetch active markets", error, { params });
|
|
1791
|
+
throw error;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Gets a single market by slug.
|
|
1796
|
+
*
|
|
1797
|
+
* @param slug - Market slug identifier
|
|
1798
|
+
* @returns Promise resolving to market details
|
|
1799
|
+
* @throws Error if API request fails or market not found
|
|
1800
|
+
*
|
|
1801
|
+
* @example
|
|
1802
|
+
* ```typescript
|
|
1803
|
+
* const market = await fetcher.getMarket('bitcoin-price-2024');
|
|
1804
|
+
* console.log(`Market: ${market.title}`);
|
|
1805
|
+
* ```
|
|
1806
|
+
*/
|
|
1807
|
+
async getMarket(slug) {
|
|
1808
|
+
this.logger.debug("Fetching market", { slug });
|
|
1809
|
+
try {
|
|
1810
|
+
const market = await this.httpClient.get(`/markets/${slug}`);
|
|
1811
|
+
this.logger.info("Market fetched successfully", {
|
|
1812
|
+
slug,
|
|
1813
|
+
title: market.title
|
|
1814
|
+
});
|
|
1815
|
+
return market;
|
|
1816
|
+
} catch (error) {
|
|
1817
|
+
this.logger.error("Failed to fetch market", error, { slug });
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Gets the orderbook for a CLOB market.
|
|
1823
|
+
*
|
|
1824
|
+
* @param slug - Market slug identifier
|
|
1825
|
+
* @returns Promise resolving to orderbook data
|
|
1826
|
+
* @throws Error if API request fails
|
|
1827
|
+
*
|
|
1828
|
+
* @example
|
|
1829
|
+
* ```typescript
|
|
1830
|
+
* const orderbook = await fetcher.getOrderBook('bitcoin-price-2024');
|
|
1831
|
+
* console.log(`Bids: ${orderbook.bids.length}, Asks: ${orderbook.asks.length}`);
|
|
1832
|
+
* ```
|
|
1833
|
+
*/
|
|
1834
|
+
async getOrderBook(slug) {
|
|
1835
|
+
this.logger.debug("Fetching orderbook", { slug });
|
|
1836
|
+
try {
|
|
1837
|
+
const orderbook = await this.httpClient.get(
|
|
1838
|
+
`/markets/${slug}/orderbook`
|
|
1839
|
+
);
|
|
1840
|
+
this.logger.info("Orderbook fetched successfully", {
|
|
1841
|
+
slug,
|
|
1842
|
+
bids: orderbook.bids.length,
|
|
1843
|
+
asks: orderbook.asks.length,
|
|
1844
|
+
tokenId: orderbook.tokenId
|
|
1845
|
+
});
|
|
1846
|
+
return orderbook;
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
this.logger.error("Failed to fetch orderbook", error, { slug });
|
|
1849
|
+
throw error;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Gets the current price for a token.
|
|
1854
|
+
*
|
|
1855
|
+
* @param tokenId - Token ID
|
|
1856
|
+
* @returns Promise resolving to price information
|
|
1857
|
+
* @throws Error if API request fails
|
|
1858
|
+
*
|
|
1859
|
+
* @example
|
|
1860
|
+
* ```typescript
|
|
1861
|
+
* const price = await fetcher.getPrice('123456');
|
|
1862
|
+
* console.log(`Current price: ${price.price}`);
|
|
1863
|
+
* ```
|
|
1864
|
+
*/
|
|
1865
|
+
async getPrice(tokenId) {
|
|
1866
|
+
this.logger.debug("Fetching price", { tokenId });
|
|
1867
|
+
try {
|
|
1868
|
+
const price = await this.httpClient.get(`/prices/${tokenId}`);
|
|
1869
|
+
this.logger.info("Price fetched successfully", {
|
|
1870
|
+
tokenId,
|
|
1871
|
+
price: price.price
|
|
1872
|
+
});
|
|
1873
|
+
return price;
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
this.logger.error("Failed to fetch price", error, { tokenId });
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
// src/portfolio/fetcher.ts
|
|
1882
|
+
var PortfolioFetcher = class {
|
|
1883
|
+
/**
|
|
1884
|
+
* Creates a new portfolio fetcher instance.
|
|
1885
|
+
*
|
|
1886
|
+
* @param httpClient - Authenticated HTTP client for API requests
|
|
1887
|
+
* @param logger - Optional logger for debugging (default: no logging)
|
|
1888
|
+
*
|
|
1889
|
+
* @example
|
|
1890
|
+
* ```typescript
|
|
1891
|
+
* // Create authenticated client
|
|
1892
|
+
* const httpClient = new HttpClient({ baseURL: API_URL });
|
|
1893
|
+
* await authenticator.authenticate({ client: 'eoa' });
|
|
1894
|
+
*
|
|
1895
|
+
* // Create portfolio fetcher
|
|
1896
|
+
* const portfolioFetcher = new PortfolioFetcher(httpClient);
|
|
1897
|
+
* ```
|
|
1898
|
+
*/
|
|
1899
|
+
constructor(httpClient, logger) {
|
|
1900
|
+
this.httpClient = httpClient;
|
|
1901
|
+
this.logger = logger || new NoOpLogger();
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Gets raw portfolio positions response from API.
|
|
1905
|
+
*
|
|
1906
|
+
* @returns Promise resolving to portfolio positions response with CLOB and AMM positions
|
|
1907
|
+
* @throws Error if API request fails or user is not authenticated
|
|
1908
|
+
*
|
|
1909
|
+
* @example
|
|
1910
|
+
* ```typescript
|
|
1911
|
+
* const response = await portfolioFetcher.getPositions();
|
|
1912
|
+
* console.log(`CLOB positions: ${response.clob.length}`);
|
|
1913
|
+
* console.log(`AMM positions: ${response.amm.length}`);
|
|
1914
|
+
* console.log(`Total points: ${response.accumulativePoints}`);
|
|
1915
|
+
* ```
|
|
1916
|
+
*/
|
|
1917
|
+
async getPositions() {
|
|
1918
|
+
this.logger.debug("Fetching user positions");
|
|
1919
|
+
try {
|
|
1920
|
+
const response = await this.httpClient.get(
|
|
1921
|
+
"/portfolio/positions"
|
|
1922
|
+
);
|
|
1923
|
+
this.logger.info("Positions fetched successfully", {
|
|
1924
|
+
clobCount: response.clob?.length || 0,
|
|
1925
|
+
ammCount: response.amm?.length || 0
|
|
1926
|
+
});
|
|
1927
|
+
return response;
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
this.logger.error("Failed to fetch positions", error);
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Gets CLOB positions only.
|
|
1935
|
+
*
|
|
1936
|
+
* @returns Promise resolving to array of CLOB positions
|
|
1937
|
+
* @throws Error if API request fails
|
|
1938
|
+
*
|
|
1939
|
+
* @example
|
|
1940
|
+
* ```typescript
|
|
1941
|
+
* const clobPositions = await portfolioFetcher.getCLOBPositions();
|
|
1942
|
+
* clobPositions.forEach(pos => {
|
|
1943
|
+
* console.log(`${pos.market.title}: YES ${pos.positions.yes.unrealizedPnl} P&L`);
|
|
1944
|
+
* });
|
|
1945
|
+
* ```
|
|
1946
|
+
*/
|
|
1947
|
+
async getCLOBPositions() {
|
|
1948
|
+
const response = await this.getPositions();
|
|
1949
|
+
return response.clob || [];
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Gets AMM positions only.
|
|
1953
|
+
*
|
|
1954
|
+
* @returns Promise resolving to array of AMM positions
|
|
1955
|
+
* @throws Error if API request fails
|
|
1956
|
+
*
|
|
1957
|
+
* @example
|
|
1958
|
+
* ```typescript
|
|
1959
|
+
* const ammPositions = await portfolioFetcher.getAMMPositions();
|
|
1960
|
+
* ammPositions.forEach(pos => {
|
|
1961
|
+
* console.log(`${pos.market.title}: ${pos.unrealizedPnl} P&L`);
|
|
1962
|
+
* });
|
|
1963
|
+
* ```
|
|
1964
|
+
*/
|
|
1965
|
+
async getAMMPositions() {
|
|
1966
|
+
const response = await this.getPositions();
|
|
1967
|
+
return response.amm || [];
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Flattens positions into a unified format for easier consumption.
|
|
1971
|
+
*
|
|
1972
|
+
* @remarks
|
|
1973
|
+
* Converts CLOB positions (which have YES/NO sides) and AMM positions
|
|
1974
|
+
* into a unified Position array. Only includes positions with non-zero values.
|
|
1975
|
+
*
|
|
1976
|
+
* @returns Promise resolving to array of flattened positions
|
|
1977
|
+
* @throws Error if API request fails
|
|
1978
|
+
*
|
|
1979
|
+
* @example
|
|
1980
|
+
* ```typescript
|
|
1981
|
+
* const positions = await portfolioFetcher.getFlattenedPositions();
|
|
1982
|
+
* positions.forEach(pos => {
|
|
1983
|
+
* const pnlPercent = (pos.unrealizedPnl / pos.costBasis) * 100;
|
|
1984
|
+
* console.log(`${pos.market.title} (${pos.side}): ${pnlPercent.toFixed(2)}% P&L`);
|
|
1985
|
+
* });
|
|
1986
|
+
* ```
|
|
1987
|
+
*/
|
|
1988
|
+
async getFlattenedPositions() {
|
|
1989
|
+
const response = await this.getPositions();
|
|
1990
|
+
const positions = [];
|
|
1991
|
+
for (const clobPos of response.clob || []) {
|
|
1992
|
+
const yesCost = parseFloat(clobPos.positions.yes.cost);
|
|
1993
|
+
const yesValue = parseFloat(clobPos.positions.yes.marketValue);
|
|
1994
|
+
if (yesCost > 0 || yesValue > 0) {
|
|
1995
|
+
positions.push({
|
|
1996
|
+
type: "CLOB",
|
|
1997
|
+
market: clobPos.market,
|
|
1998
|
+
side: "YES",
|
|
1999
|
+
costBasis: yesCost,
|
|
2000
|
+
marketValue: yesValue,
|
|
2001
|
+
unrealizedPnl: parseFloat(clobPos.positions.yes.unrealizedPnl),
|
|
2002
|
+
realizedPnl: parseFloat(clobPos.positions.yes.realisedPnl),
|
|
2003
|
+
currentPrice: clobPos.latestTrade?.latestYesPrice ?? 0,
|
|
2004
|
+
avgPrice: yesCost > 0 ? parseFloat(clobPos.positions.yes.fillPrice) / 1e6 : 0,
|
|
2005
|
+
tokenBalance: parseFloat(clobPos.tokensBalance.yes)
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
const noCost = parseFloat(clobPos.positions.no.cost);
|
|
2009
|
+
const noValue = parseFloat(clobPos.positions.no.marketValue);
|
|
2010
|
+
if (noCost > 0 || noValue > 0) {
|
|
2011
|
+
positions.push({
|
|
2012
|
+
type: "CLOB",
|
|
2013
|
+
market: clobPos.market,
|
|
2014
|
+
side: "NO",
|
|
2015
|
+
costBasis: noCost,
|
|
2016
|
+
marketValue: noValue,
|
|
2017
|
+
unrealizedPnl: parseFloat(clobPos.positions.no.unrealizedPnl),
|
|
2018
|
+
realizedPnl: parseFloat(clobPos.positions.no.realisedPnl),
|
|
2019
|
+
currentPrice: clobPos.latestTrade?.latestNoPrice ?? 0,
|
|
2020
|
+
avgPrice: noCost > 0 ? parseFloat(clobPos.positions.no.fillPrice) / 1e6 : 0,
|
|
2021
|
+
tokenBalance: parseFloat(clobPos.tokensBalance.no)
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
for (const ammPos of response.amm || []) {
|
|
2026
|
+
const cost = parseFloat(ammPos.totalBuysCost);
|
|
2027
|
+
const value = parseFloat(ammPos.collateralAmount);
|
|
2028
|
+
if (cost > 0 || value > 0) {
|
|
2029
|
+
positions.push({
|
|
2030
|
+
type: "AMM",
|
|
2031
|
+
market: ammPos.market,
|
|
2032
|
+
side: ammPos.outcomeIndex === 0 ? "YES" : "NO",
|
|
2033
|
+
costBasis: cost,
|
|
2034
|
+
marketValue: value,
|
|
2035
|
+
unrealizedPnl: parseFloat(ammPos.unrealizedPnl),
|
|
2036
|
+
realizedPnl: parseFloat(ammPos.realizedPnl),
|
|
2037
|
+
currentPrice: ammPos.latestTrade ? parseFloat(ammPos.latestTrade.outcomeTokenPrice) : 0,
|
|
2038
|
+
avgPrice: parseFloat(ammPos.averageFillPrice),
|
|
2039
|
+
tokenBalance: parseFloat(ammPos.outcomeTokenAmount)
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
this.logger.debug("Flattened positions", { count: positions.length });
|
|
2044
|
+
return positions;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Calculates portfolio summary statistics from raw API response.
|
|
2048
|
+
*
|
|
2049
|
+
* @param response - Portfolio positions response from API
|
|
2050
|
+
* @returns Portfolio summary with totals and statistics
|
|
2051
|
+
*
|
|
2052
|
+
* @example
|
|
2053
|
+
* ```typescript
|
|
2054
|
+
* const response = await portfolioFetcher.getPositions();
|
|
2055
|
+
* const summary = portfolioFetcher.calculateSummary(response);
|
|
2056
|
+
*
|
|
2057
|
+
* console.log(`Total Portfolio Value: $${(summary.totalValue / 1e6).toFixed(2)}`);
|
|
2058
|
+
* console.log(`Total P&L: ${summary.totalUnrealizedPnlPercent.toFixed(2)}%`);
|
|
2059
|
+
* console.log(`CLOB Positions: ${summary.breakdown.clob.positions}`);
|
|
2060
|
+
* console.log(`AMM Positions: ${summary.breakdown.amm.positions}`);
|
|
2061
|
+
* ```
|
|
2062
|
+
*/
|
|
2063
|
+
calculateSummary(response) {
|
|
2064
|
+
this.logger.debug("Calculating portfolio summary", {
|
|
2065
|
+
clobCount: response.clob?.length || 0,
|
|
2066
|
+
ammCount: response.amm?.length || 0
|
|
2067
|
+
});
|
|
2068
|
+
let totalValue = 0;
|
|
2069
|
+
let totalCostBasis = 0;
|
|
2070
|
+
let totalUnrealizedPnl = 0;
|
|
2071
|
+
let totalRealizedPnl = 0;
|
|
2072
|
+
let clobPositions = 0;
|
|
2073
|
+
let clobValue = 0;
|
|
2074
|
+
let clobPnl = 0;
|
|
2075
|
+
let ammPositions = 0;
|
|
2076
|
+
let ammValue = 0;
|
|
2077
|
+
let ammPnl = 0;
|
|
2078
|
+
for (const clobPos of response.clob || []) {
|
|
2079
|
+
const yesCost = parseFloat(clobPos.positions.yes.cost);
|
|
2080
|
+
const yesValue = parseFloat(clobPos.positions.yes.marketValue);
|
|
2081
|
+
const yesUnrealizedPnl = parseFloat(clobPos.positions.yes.unrealizedPnl);
|
|
2082
|
+
const yesRealizedPnl = parseFloat(clobPos.positions.yes.realisedPnl);
|
|
2083
|
+
if (yesCost > 0 || yesValue > 0) {
|
|
2084
|
+
clobPositions++;
|
|
2085
|
+
totalCostBasis += yesCost;
|
|
2086
|
+
totalValue += yesValue;
|
|
2087
|
+
totalUnrealizedPnl += yesUnrealizedPnl;
|
|
2088
|
+
totalRealizedPnl += yesRealizedPnl;
|
|
2089
|
+
clobValue += yesValue;
|
|
2090
|
+
clobPnl += yesUnrealizedPnl;
|
|
2091
|
+
}
|
|
2092
|
+
const noCost = parseFloat(clobPos.positions.no.cost);
|
|
2093
|
+
const noValue = parseFloat(clobPos.positions.no.marketValue);
|
|
2094
|
+
const noUnrealizedPnl = parseFloat(clobPos.positions.no.unrealizedPnl);
|
|
2095
|
+
const noRealizedPnl = parseFloat(clobPos.positions.no.realisedPnl);
|
|
2096
|
+
if (noCost > 0 || noValue > 0) {
|
|
2097
|
+
clobPositions++;
|
|
2098
|
+
totalCostBasis += noCost;
|
|
2099
|
+
totalValue += noValue;
|
|
2100
|
+
totalUnrealizedPnl += noUnrealizedPnl;
|
|
2101
|
+
totalRealizedPnl += noRealizedPnl;
|
|
2102
|
+
clobValue += noValue;
|
|
2103
|
+
clobPnl += noUnrealizedPnl;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
for (const ammPos of response.amm || []) {
|
|
2107
|
+
const cost = parseFloat(ammPos.totalBuysCost);
|
|
2108
|
+
const value = parseFloat(ammPos.collateralAmount);
|
|
2109
|
+
const unrealizedPnl = parseFloat(ammPos.unrealizedPnl);
|
|
2110
|
+
const realizedPnl = parseFloat(ammPos.realizedPnl);
|
|
2111
|
+
if (cost > 0 || value > 0) {
|
|
2112
|
+
ammPositions++;
|
|
2113
|
+
totalCostBasis += cost;
|
|
2114
|
+
totalValue += value;
|
|
2115
|
+
totalUnrealizedPnl += unrealizedPnl;
|
|
2116
|
+
totalRealizedPnl += realizedPnl;
|
|
2117
|
+
ammValue += value;
|
|
2118
|
+
ammPnl += unrealizedPnl;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const totalUnrealizedPnlPercent = totalCostBasis > 0 ? totalUnrealizedPnl / totalCostBasis * 100 : 0;
|
|
2122
|
+
const uniqueMarkets = /* @__PURE__ */ new Set();
|
|
2123
|
+
for (const pos of response.clob || []) {
|
|
2124
|
+
uniqueMarkets.add(pos.market.id);
|
|
2125
|
+
}
|
|
2126
|
+
for (const pos of response.amm || []) {
|
|
2127
|
+
uniqueMarkets.add(pos.market.id);
|
|
2128
|
+
}
|
|
2129
|
+
const summary = {
|
|
2130
|
+
totalValue,
|
|
2131
|
+
totalCostBasis,
|
|
2132
|
+
totalUnrealizedPnl,
|
|
2133
|
+
totalRealizedPnl,
|
|
2134
|
+
totalUnrealizedPnlPercent,
|
|
2135
|
+
positionCount: clobPositions + ammPositions,
|
|
2136
|
+
marketCount: uniqueMarkets.size,
|
|
2137
|
+
breakdown: {
|
|
2138
|
+
clob: {
|
|
2139
|
+
positions: clobPositions,
|
|
2140
|
+
value: clobValue,
|
|
2141
|
+
pnl: clobPnl
|
|
2142
|
+
},
|
|
2143
|
+
amm: {
|
|
2144
|
+
positions: ammPositions,
|
|
2145
|
+
value: ammValue,
|
|
2146
|
+
pnl: ammPnl
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
this.logger.debug("Portfolio summary calculated", summary);
|
|
2151
|
+
return summary;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Gets positions and calculates summary in a single call.
|
|
2155
|
+
*
|
|
2156
|
+
* @returns Promise resolving to response and summary
|
|
2157
|
+
* @throws Error if API request fails or user is not authenticated
|
|
2158
|
+
*
|
|
2159
|
+
* @example
|
|
2160
|
+
* ```typescript
|
|
2161
|
+
* const { response, summary } = await portfolioFetcher.getPortfolio();
|
|
2162
|
+
*
|
|
2163
|
+
* console.log('Portfolio Summary:');
|
|
2164
|
+
* console.log(` Total Value: $${(summary.totalValue / 1e6).toFixed(2)}`);
|
|
2165
|
+
* console.log(` Total P&L: $${(summary.totalUnrealizedPnl / 1e6).toFixed(2)}`);
|
|
2166
|
+
* console.log(` P&L %: ${summary.totalUnrealizedPnlPercent.toFixed(2)}%`);
|
|
2167
|
+
* console.log(`\nCLOB Positions: ${response.clob.length}`);
|
|
2168
|
+
* console.log(`AMM Positions: ${response.amm.length}`);
|
|
2169
|
+
* ```
|
|
2170
|
+
*/
|
|
2171
|
+
async getPortfolio() {
|
|
2172
|
+
this.logger.debug("Fetching portfolio with summary");
|
|
2173
|
+
const response = await this.getPositions();
|
|
2174
|
+
const summary = this.calculateSummary(response);
|
|
2175
|
+
this.logger.info("Portfolio fetched with summary", {
|
|
2176
|
+
positionCount: summary.positionCount,
|
|
2177
|
+
totalValueUSDC: summary.totalValue / 1e6,
|
|
2178
|
+
pnlPercent: summary.totalUnrealizedPnlPercent
|
|
2179
|
+
});
|
|
2180
|
+
return { response, summary };
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
// src/websocket/client.ts
|
|
2185
|
+
var import_socket = require("socket.io-client");
|
|
2186
|
+
var WebSocketClient = class {
|
|
2187
|
+
/**
|
|
2188
|
+
* Creates a new WebSocket client.
|
|
2189
|
+
*
|
|
2190
|
+
* @param config - WebSocket configuration
|
|
2191
|
+
* @param logger - Optional logger for debugging
|
|
2192
|
+
*/
|
|
2193
|
+
constructor(config = {}, logger) {
|
|
2194
|
+
this.socket = null;
|
|
2195
|
+
this.state = "disconnected" /* DISCONNECTED */;
|
|
2196
|
+
this.reconnectAttempts = 0;
|
|
2197
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
2198
|
+
this.pendingListeners = [];
|
|
2199
|
+
this.config = {
|
|
2200
|
+
url: config.url || DEFAULT_WS_URL,
|
|
2201
|
+
sessionCookie: config.sessionCookie || "",
|
|
2202
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
2203
|
+
reconnectDelay: config.reconnectDelay || 1e3,
|
|
2204
|
+
maxReconnectAttempts: config.maxReconnectAttempts || Infinity,
|
|
2205
|
+
timeout: config.timeout || 1e4
|
|
2206
|
+
};
|
|
2207
|
+
this.logger = logger || new NoOpLogger();
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Gets current connection state.
|
|
2211
|
+
*
|
|
2212
|
+
* @returns Current WebSocket state
|
|
2213
|
+
*/
|
|
2214
|
+
getState() {
|
|
2215
|
+
return this.state;
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Checks if client is connected.
|
|
2219
|
+
*
|
|
2220
|
+
* @returns True if connected
|
|
2221
|
+
*/
|
|
2222
|
+
isConnected() {
|
|
2223
|
+
return this.state === "connected" /* CONNECTED */ && this.socket?.connected === true;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Sets the session cookie for authentication.
|
|
2227
|
+
*
|
|
2228
|
+
* @param sessionCookie - Session cookie value
|
|
2229
|
+
*/
|
|
2230
|
+
setSessionCookie(sessionCookie) {
|
|
2231
|
+
this.config.sessionCookie = sessionCookie;
|
|
2232
|
+
if (this.socket?.connected) {
|
|
2233
|
+
this.logger.info("Session cookie updated, reconnecting...");
|
|
2234
|
+
this.disconnect();
|
|
2235
|
+
this.connect();
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Connects to the WebSocket server.
|
|
2240
|
+
*
|
|
2241
|
+
* @returns Promise that resolves when connected
|
|
2242
|
+
* @throws Error if connection fails
|
|
2243
|
+
*
|
|
2244
|
+
* @example
|
|
2245
|
+
* ```typescript
|
|
2246
|
+
* await wsClient.connect();
|
|
2247
|
+
* console.log('Connected!');
|
|
2248
|
+
* ```
|
|
2249
|
+
*/
|
|
2250
|
+
async connect() {
|
|
2251
|
+
if (this.socket?.connected) {
|
|
2252
|
+
this.logger.info("Already connected");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
this.logger.info("Connecting to WebSocket", { url: this.config.url });
|
|
2256
|
+
this.state = "connecting" /* CONNECTING */;
|
|
2257
|
+
return new Promise((resolve, reject) => {
|
|
2258
|
+
const timeout = setTimeout(() => {
|
|
2259
|
+
reject(new Error(`Connection timeout after ${this.config.timeout}ms`));
|
|
2260
|
+
}, this.config.timeout);
|
|
2261
|
+
const wsUrl = this.config.url.endsWith("/markets") ? this.config.url : `${this.config.url}/markets`;
|
|
2262
|
+
this.socket = (0, import_socket.io)(wsUrl, {
|
|
2263
|
+
transports: ["websocket"],
|
|
2264
|
+
// Use WebSocket transport only
|
|
2265
|
+
extraHeaders: {
|
|
2266
|
+
cookie: `limitless_session=${this.config.sessionCookie}`
|
|
2267
|
+
},
|
|
2268
|
+
reconnection: this.config.autoReconnect,
|
|
2269
|
+
reconnectionDelay: this.config.reconnectDelay,
|
|
2270
|
+
reconnectionAttempts: this.config.maxReconnectAttempts,
|
|
2271
|
+
timeout: this.config.timeout
|
|
2272
|
+
});
|
|
2273
|
+
this.attachPendingListeners();
|
|
2274
|
+
this.setupEventHandlers();
|
|
2275
|
+
this.socket.once("connect", () => {
|
|
2276
|
+
clearTimeout(timeout);
|
|
2277
|
+
this.state = "connected" /* CONNECTED */;
|
|
2278
|
+
this.reconnectAttempts = 0;
|
|
2279
|
+
this.logger.info("WebSocket connected");
|
|
2280
|
+
this.resubscribeAll();
|
|
2281
|
+
resolve();
|
|
2282
|
+
});
|
|
2283
|
+
this.socket.once("connect_error", (error) => {
|
|
2284
|
+
clearTimeout(timeout);
|
|
2285
|
+
this.state = "error" /* ERROR */;
|
|
2286
|
+
this.logger.error("WebSocket connection error", error);
|
|
2287
|
+
reject(error);
|
|
2288
|
+
});
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Disconnects from the WebSocket server.
|
|
2293
|
+
*
|
|
2294
|
+
* @example
|
|
2295
|
+
* ```typescript
|
|
2296
|
+
* wsClient.disconnect();
|
|
2297
|
+
* ```
|
|
2298
|
+
*/
|
|
2299
|
+
disconnect() {
|
|
2300
|
+
if (!this.socket) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
this.logger.info("Disconnecting from WebSocket");
|
|
2304
|
+
this.socket.disconnect();
|
|
2305
|
+
this.socket = null;
|
|
2306
|
+
this.state = "disconnected" /* DISCONNECTED */;
|
|
2307
|
+
this.subscriptions.clear();
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Subscribes to a channel.
|
|
2311
|
+
*
|
|
2312
|
+
* @param channel - Channel to subscribe to
|
|
2313
|
+
* @param options - Subscription options
|
|
2314
|
+
* @returns Promise that resolves when subscribed
|
|
2315
|
+
* @throws Error if not connected
|
|
2316
|
+
*
|
|
2317
|
+
* @example
|
|
2318
|
+
* ```typescript
|
|
2319
|
+
* // Subscribe to orderbook for a specific market
|
|
2320
|
+
* await wsClient.subscribe('orderbook', { marketSlug: 'market-123' });
|
|
2321
|
+
*
|
|
2322
|
+
* // Subscribe to all trades
|
|
2323
|
+
* await wsClient.subscribe('trades');
|
|
2324
|
+
*
|
|
2325
|
+
* // Subscribe to your orders
|
|
2326
|
+
* await wsClient.subscribe('orders');
|
|
2327
|
+
* ```
|
|
2328
|
+
*/
|
|
2329
|
+
async subscribe(channel, options = {}) {
|
|
2330
|
+
if (!this.isConnected()) {
|
|
2331
|
+
throw new Error("WebSocket not connected. Call connect() first.");
|
|
2332
|
+
}
|
|
2333
|
+
const subscriptionKey = this.getSubscriptionKey(channel, options);
|
|
2334
|
+
this.subscriptions.set(subscriptionKey, options);
|
|
2335
|
+
this.logger.info("Subscribing to channel", { channel, options });
|
|
2336
|
+
return new Promise((resolve, reject) => {
|
|
2337
|
+
this.socket.emit(channel, options, (response) => {
|
|
2338
|
+
if (response?.error) {
|
|
2339
|
+
this.logger.error("Subscription failed", response.error);
|
|
2340
|
+
this.subscriptions.delete(subscriptionKey);
|
|
2341
|
+
reject(new Error(response.error));
|
|
2342
|
+
} else {
|
|
2343
|
+
this.logger.info("Subscribed successfully", { channel, options });
|
|
2344
|
+
resolve();
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Unsubscribes from a channel.
|
|
2351
|
+
*
|
|
2352
|
+
* @param channel - Channel to unsubscribe from
|
|
2353
|
+
* @param options - Subscription options (must match subscribe call)
|
|
2354
|
+
* @returns Promise that resolves when unsubscribed
|
|
2355
|
+
*
|
|
2356
|
+
* @example
|
|
2357
|
+
* ```typescript
|
|
2358
|
+
* await wsClient.unsubscribe('orderbook', { marketSlug: 'market-123' });
|
|
2359
|
+
* ```
|
|
2360
|
+
*/
|
|
2361
|
+
async unsubscribe(channel, options = {}) {
|
|
2362
|
+
if (!this.isConnected()) {
|
|
2363
|
+
throw new Error("WebSocket not connected");
|
|
2364
|
+
}
|
|
2365
|
+
const subscriptionKey = this.getSubscriptionKey(channel, options);
|
|
2366
|
+
this.subscriptions.delete(subscriptionKey);
|
|
2367
|
+
this.logger.info("Unsubscribing from channel", { channel, options });
|
|
2368
|
+
return new Promise((resolve, reject) => {
|
|
2369
|
+
this.socket.emit("unsubscribe", { channel, ...options }, (response) => {
|
|
2370
|
+
if (response?.error) {
|
|
2371
|
+
this.logger.error("Unsubscribe failed", response.error);
|
|
2372
|
+
reject(new Error(response.error));
|
|
2373
|
+
} else {
|
|
2374
|
+
this.logger.info("Unsubscribed successfully", { channel, options });
|
|
2375
|
+
resolve();
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Registers an event listener.
|
|
2382
|
+
*
|
|
2383
|
+
* @param event - Event name
|
|
2384
|
+
* @param handler - Event handler
|
|
2385
|
+
* @returns This client for chaining
|
|
2386
|
+
*
|
|
2387
|
+
* @example
|
|
2388
|
+
* ```typescript
|
|
2389
|
+
* wsClient
|
|
2390
|
+
* .on('orderbook', (data) => console.log('Orderbook:', data))
|
|
2391
|
+
* .on('trade', (data) => console.log('Trade:', data))
|
|
2392
|
+
* .on('error', (error) => console.error('Error:', error));
|
|
2393
|
+
* ```
|
|
2394
|
+
*/
|
|
2395
|
+
on(event, handler) {
|
|
2396
|
+
if (!this.socket) {
|
|
2397
|
+
this.pendingListeners.push({ event, handler });
|
|
2398
|
+
return this;
|
|
2399
|
+
}
|
|
2400
|
+
this.socket.on(event, handler);
|
|
2401
|
+
return this;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Registers a one-time event listener.
|
|
2405
|
+
*
|
|
2406
|
+
* @param event - Event name
|
|
2407
|
+
* @param handler - Event handler
|
|
2408
|
+
* @returns This client for chaining
|
|
2409
|
+
*/
|
|
2410
|
+
once(event, handler) {
|
|
2411
|
+
if (!this.socket) {
|
|
2412
|
+
throw new Error("WebSocket not initialized. Call connect() first.");
|
|
2413
|
+
}
|
|
2414
|
+
this.socket.once(event, handler);
|
|
2415
|
+
return this;
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Removes an event listener.
|
|
2419
|
+
*
|
|
2420
|
+
* @param event - Event name
|
|
2421
|
+
* @param handler - Event handler to remove
|
|
2422
|
+
* @returns This client for chaining
|
|
2423
|
+
*/
|
|
2424
|
+
off(event, handler) {
|
|
2425
|
+
if (!this.socket) {
|
|
2426
|
+
return this;
|
|
2427
|
+
}
|
|
2428
|
+
this.socket.off(event, handler);
|
|
2429
|
+
return this;
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Attach any pending event listeners that were added before connect().
|
|
2433
|
+
* @internal
|
|
2434
|
+
*/
|
|
2435
|
+
attachPendingListeners() {
|
|
2436
|
+
if (!this.socket || this.pendingListeners.length === 0) {
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
for (const { event, handler } of this.pendingListeners) {
|
|
2440
|
+
this.socket.on(event, handler);
|
|
2441
|
+
}
|
|
2442
|
+
this.pendingListeners = [];
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Setup internal event handlers for connection management.
|
|
2446
|
+
* @internal
|
|
2447
|
+
*/
|
|
2448
|
+
setupEventHandlers() {
|
|
2449
|
+
if (!this.socket) {
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
this.socket.on("connect", () => {
|
|
2453
|
+
this.state = "connected" /* CONNECTED */;
|
|
2454
|
+
this.reconnectAttempts = 0;
|
|
2455
|
+
this.logger.info("WebSocket connected");
|
|
2456
|
+
});
|
|
2457
|
+
this.socket.on("disconnect", (reason) => {
|
|
2458
|
+
this.state = "disconnected" /* DISCONNECTED */;
|
|
2459
|
+
this.logger.info("WebSocket disconnected", { reason });
|
|
2460
|
+
});
|
|
2461
|
+
this.socket.on("error", (error) => {
|
|
2462
|
+
this.state = "error" /* ERROR */;
|
|
2463
|
+
this.logger.error("WebSocket error", error);
|
|
2464
|
+
});
|
|
2465
|
+
this.socket.io.on("reconnect_attempt", (attempt) => {
|
|
2466
|
+
this.state = "reconnecting" /* RECONNECTING */;
|
|
2467
|
+
this.reconnectAttempts = attempt;
|
|
2468
|
+
this.logger.info("Reconnecting...", { attempt });
|
|
2469
|
+
});
|
|
2470
|
+
this.socket.io.on("reconnect", (attempt) => {
|
|
2471
|
+
this.state = "connected" /* CONNECTED */;
|
|
2472
|
+
this.logger.info("Reconnected", { attempts: attempt });
|
|
2473
|
+
this.resubscribeAll();
|
|
2474
|
+
});
|
|
2475
|
+
this.socket.io.on("reconnect_error", (error) => {
|
|
2476
|
+
this.logger.error("Reconnection error", error);
|
|
2477
|
+
});
|
|
2478
|
+
this.socket.io.on("reconnect_failed", () => {
|
|
2479
|
+
this.state = "error" /* ERROR */;
|
|
2480
|
+
this.logger.error("Reconnection failed");
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Re-subscribes to all previous subscriptions after reconnection.
|
|
2485
|
+
* @internal
|
|
2486
|
+
*/
|
|
2487
|
+
async resubscribeAll() {
|
|
2488
|
+
if (this.subscriptions.size === 0) {
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
this.logger.info("Re-subscribing to channels", {
|
|
2492
|
+
count: this.subscriptions.size
|
|
2493
|
+
});
|
|
2494
|
+
for (const [key, options] of this.subscriptions.entries()) {
|
|
2495
|
+
const channel = this.getChannelFromKey(key);
|
|
2496
|
+
try {
|
|
2497
|
+
await this.subscribe(channel, options);
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
this.logger.error("Failed to re-subscribe", error, { channel, options });
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Creates a unique subscription key.
|
|
2505
|
+
* @internal
|
|
2506
|
+
*/
|
|
2507
|
+
getSubscriptionKey(channel, options) {
|
|
2508
|
+
return `${channel}:${options.marketSlug || "global"}`;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Extracts channel from subscription key.
|
|
2512
|
+
* @internal
|
|
2513
|
+
*/
|
|
2514
|
+
getChannelFromKey(key) {
|
|
2515
|
+
return key.split(":")[0];
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2519
|
+
0 && (module.exports = {
|
|
2520
|
+
APIError,
|
|
2521
|
+
AuthenticatedClient,
|
|
2522
|
+
Authenticator,
|
|
2523
|
+
BASE_SEPOLIA_CHAIN_ID,
|
|
2524
|
+
CONTRACT_ADDRESSES,
|
|
2525
|
+
ConsoleLogger,
|
|
2526
|
+
DEFAULT_API_URL,
|
|
2527
|
+
DEFAULT_CHAIN_ID,
|
|
2528
|
+
DEFAULT_WS_URL,
|
|
2529
|
+
HttpClient,
|
|
2530
|
+
MarketFetcher,
|
|
2531
|
+
MarketType,
|
|
2532
|
+
MessageSigner,
|
|
2533
|
+
NoOpLogger,
|
|
2534
|
+
OrderBuilder,
|
|
2535
|
+
OrderClient,
|
|
2536
|
+
OrderSigner,
|
|
2537
|
+
OrderType,
|
|
2538
|
+
PortfolioFetcher,
|
|
2539
|
+
RetryConfig,
|
|
2540
|
+
RetryableClient,
|
|
2541
|
+
SIGNING_MESSAGE_TEMPLATE,
|
|
2542
|
+
Side,
|
|
2543
|
+
SignatureType,
|
|
2544
|
+
ValidationError,
|
|
2545
|
+
WebSocketClient,
|
|
2546
|
+
WebSocketState,
|
|
2547
|
+
ZERO_ADDRESS,
|
|
2548
|
+
getContractAddress,
|
|
2549
|
+
retryOnErrors,
|
|
2550
|
+
validateOrderArgs,
|
|
2551
|
+
validateSignedOrder,
|
|
2552
|
+
validateUnsignedOrder,
|
|
2553
|
+
withRetry
|
|
2554
|
+
});
|
|
2555
|
+
//# sourceMappingURL=index.js.map
|