@vesper85/strategy-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +448 -0
- package/dist/clients/hyperliquid-client.d.ts +68 -0
- package/dist/clients/polymarket-client.d.ts +157 -0
- package/dist/context/osiris-context.d.ts +14 -0
- package/dist/engine/event-runner.d.ts +129 -0
- package/dist/engine/strategy-runner.d.ts +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2195 -0
- package/dist/signer/index.d.ts +54 -0
- package/dist/signer/osiris-signer.d.ts +29 -0
- package/dist/signer/privatekey-signer.d.ts +41 -0
- package/dist/signer/types.d.ts +62 -0
- package/dist/state/memory-state.d.ts +17 -0
- package/dist/state/redis-state.d.ts +20 -0
- package/dist/types/event-types.d.ts +102 -0
- package/dist/types/osiris.d.ts +414 -0
- package/dist/types/strategy.d.ts +35 -0
- package/dist/utils/logger.d.ts +15 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2195 @@
|
|
|
1
|
+
import { PolymarketGammaClient } from 'polymarket-gamma';
|
|
2
|
+
import { ClobClient, Side } from '@polymarket/clob-client';
|
|
3
|
+
import { MaxUint256, ethers } from 'ethers';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { serializeSignature, createWalletClient, http } from 'viem';
|
|
6
|
+
import { connectMCPServer, removeConnection } from '@osiris-ai/agent-sdk';
|
|
7
|
+
import { Hyperliquid } from 'hyperliquid';
|
|
8
|
+
import { createClient } from 'redis';
|
|
9
|
+
import superjson from 'superjson';
|
|
10
|
+
import { TechnicalAnalysisService } from '@vesper85/technical-indicators';
|
|
11
|
+
import WebSocket from 'ws';
|
|
12
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
|
+
import { sepolia, base, optimism, arbitrum, polygon, mainnet } from 'viem/chains';
|
|
14
|
+
|
|
15
|
+
// @osiris-ai/strategy-sdk - Strategy SDK for Trading
|
|
16
|
+
|
|
17
|
+
var OsirisSigner = class {
|
|
18
|
+
hubBaseUrl;
|
|
19
|
+
accessToken;
|
|
20
|
+
connectionId;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.hubBaseUrl = config.hubBaseUrl.replace(/\/$/, "");
|
|
23
|
+
this.accessToken = config.accessToken;
|
|
24
|
+
this.connectionId = config.connectionId;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sign a transaction via Osiris Hub API
|
|
28
|
+
* @param walletAddress - Required wallet address for signing
|
|
29
|
+
*/
|
|
30
|
+
async signTransaction(transaction, chain, walletAddress) {
|
|
31
|
+
try {
|
|
32
|
+
const serializedTx = typeof transaction === "string" ? transaction : JSON.stringify(transaction);
|
|
33
|
+
const response = await axios.post(
|
|
34
|
+
`${this.hubBaseUrl}/hub/wallet/sign`,
|
|
35
|
+
{
|
|
36
|
+
id: this.connectionId,
|
|
37
|
+
method: "signTransaction",
|
|
38
|
+
walletAddress,
|
|
39
|
+
chain,
|
|
40
|
+
payload: {
|
|
41
|
+
serializedTransaction: serializedTx
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
48
|
+
},
|
|
49
|
+
timeout: 3e4
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
if (response.data.status !== "SUCCESS" && response.data.status !== "success") {
|
|
53
|
+
throw new Error(response.data.error || "Failed to sign transaction");
|
|
54
|
+
}
|
|
55
|
+
const signedTx = response.data.data?.signedTransaction || response.data.data?.data?.signedTransaction;
|
|
56
|
+
if (!signedTx) {
|
|
57
|
+
throw new Error("No signed transaction in response");
|
|
58
|
+
}
|
|
59
|
+
return signedTx.startsWith("0x") ? signedTx : `0x${signedTx}`;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(`Osiris signTransaction failed: ${error.message || error}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sign typed data (EIP-712) via Osiris Hub API
|
|
66
|
+
* @param walletAddress - Required wallet address for signing
|
|
67
|
+
*/
|
|
68
|
+
async signTypedData(typedData, chain, walletAddress) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await axios.post(
|
|
71
|
+
`${this.hubBaseUrl}/hub/wallet/sign`,
|
|
72
|
+
{
|
|
73
|
+
id: this.connectionId,
|
|
74
|
+
method: "signTypedData",
|
|
75
|
+
walletAddress,
|
|
76
|
+
chain,
|
|
77
|
+
payload: [typedData]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
83
|
+
},
|
|
84
|
+
timeout: 3e4
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
if (response.data.status !== "SUCCESS" && response.data.status !== "success") {
|
|
88
|
+
throw new Error(response.data.error || "Failed to sign typed data");
|
|
89
|
+
}
|
|
90
|
+
const signatures = response.data.data?.signatures || response.data.data?.data?.signatures;
|
|
91
|
+
if (!signatures || signatures.length === 0) {
|
|
92
|
+
throw new Error("No signature in response");
|
|
93
|
+
}
|
|
94
|
+
const signature = signatures[0];
|
|
95
|
+
if (typeof signature === "string") {
|
|
96
|
+
return signature.startsWith("0x") ? signature : `0x${signature}`;
|
|
97
|
+
}
|
|
98
|
+
if (signature.r && signature.s && signature.v !== void 0) {
|
|
99
|
+
return serializeSignature({
|
|
100
|
+
r: signature.r.startsWith("0x") ? signature.r : `0x${signature.r}`,
|
|
101
|
+
s: signature.s.startsWith("0x") ? signature.s : `0x${signature.s}`,
|
|
102
|
+
yParity: Number(signature.v)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
throw new Error("Invalid signature format in response");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Osiris signTypedData failed: ${error.message || error}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Sign a message via Osiris Hub API
|
|
112
|
+
* @param walletAddress - Required wallet address for signing
|
|
113
|
+
*/
|
|
114
|
+
async signMessage(message, chain, walletAddress) {
|
|
115
|
+
try {
|
|
116
|
+
const messageStr = typeof message === "string" ? message : Buffer.from(message).toString("hex");
|
|
117
|
+
const response = await axios.post(
|
|
118
|
+
`${this.hubBaseUrl}/hub/wallet/sign`,
|
|
119
|
+
{
|
|
120
|
+
id: this.connectionId,
|
|
121
|
+
method: "signMessage",
|
|
122
|
+
walletAddress,
|
|
123
|
+
chain,
|
|
124
|
+
payload: [messageStr]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
130
|
+
},
|
|
131
|
+
timeout: 3e4
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
if (response.data.status !== "SUCCESS" && response.data.status !== "success") {
|
|
135
|
+
throw new Error(response.data.error || "Failed to sign message");
|
|
136
|
+
}
|
|
137
|
+
const signatures = response.data.data?.signatures || response.data.data?.data?.signatures;
|
|
138
|
+
if (!signatures || signatures.length === 0) {
|
|
139
|
+
throw new Error("No signature in response");
|
|
140
|
+
}
|
|
141
|
+
const signature = signatures[0];
|
|
142
|
+
return typeof signature === "string" ? signature.startsWith("0x") ? signature : `0x${signature}` : signature;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(`Osiris signMessage failed: ${error.message || error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
getPrivateKey() {
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var MaxUint256BigInt = BigInt(MaxUint256.toString());
|
|
152
|
+
var POLYGON_CHAIN_ID = 137;
|
|
153
|
+
var USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
|
|
154
|
+
var CTF_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
|
|
155
|
+
var CTF_EXCHANGE_ADDRESS = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E";
|
|
156
|
+
var DEFAULT_CLOB_HOST = "https://clob.polymarket.com";
|
|
157
|
+
var POLYGON_RPC_URL = "https://polygon-rpc.com";
|
|
158
|
+
var ERC20_ABI = [
|
|
159
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
160
|
+
"function allowance(address owner, address spender) external view returns (uint256)",
|
|
161
|
+
"function balanceOf(address account) external view returns (uint256)"
|
|
162
|
+
];
|
|
163
|
+
var ERC1155_ABI = [
|
|
164
|
+
"function setApprovalForAll(address operator, bool approved) external",
|
|
165
|
+
"function isApprovedForAll(address account, address operator) external view returns (bool)"
|
|
166
|
+
];
|
|
167
|
+
var PolymarketClient = class {
|
|
168
|
+
constructor(logger, signer, userAddress, options) {
|
|
169
|
+
this.logger = logger;
|
|
170
|
+
this.signer = signer;
|
|
171
|
+
this.userAddress = userAddress;
|
|
172
|
+
this.gammaClient = new PolymarketGammaClient();
|
|
173
|
+
this.provider = new ethers.JsonRpcProvider(POLYGON_RPC_URL);
|
|
174
|
+
this.isOsirisSigner = signer instanceof OsirisSigner || signer.impl instanceof OsirisSigner || signer.getPrivateKey() === void 0;
|
|
175
|
+
this.options = {
|
|
176
|
+
chainId: options?.chainId ?? POLYGON_CHAIN_ID,
|
|
177
|
+
autoApprove: options?.autoApprove ?? false,
|
|
178
|
+
clobHost: options?.clobHost ?? DEFAULT_CLOB_HOST,
|
|
179
|
+
...options?.mcpUrl && { mcpUrl: options.mcpUrl },
|
|
180
|
+
...options?.mcpAccessToken && { mcpAccessToken: options.mcpAccessToken },
|
|
181
|
+
...options?.useMcpForOrders !== void 0 && { useMcpForOrders: options.useMcpForOrders }
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
gammaClient;
|
|
185
|
+
clobClient = null;
|
|
186
|
+
wallet = null;
|
|
187
|
+
provider;
|
|
188
|
+
options;
|
|
189
|
+
tradingInitialized = false;
|
|
190
|
+
mcpInstance = null;
|
|
191
|
+
isOsirisSigner = false;
|
|
192
|
+
userAddress;
|
|
193
|
+
// ============================================
|
|
194
|
+
// CLOB Client Initialization
|
|
195
|
+
// ============================================
|
|
196
|
+
async initializeTradingClient() {
|
|
197
|
+
if (this.tradingInitialized) {
|
|
198
|
+
this.logger.info("Trading client already initialized");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!this.signer) {
|
|
202
|
+
throw new Error("Signer is required to initialize trading client");
|
|
203
|
+
}
|
|
204
|
+
const address = this.userAddress;
|
|
205
|
+
if (!address) {
|
|
206
|
+
throw new Error("userAddress is required to initialize trading client");
|
|
207
|
+
}
|
|
208
|
+
const shouldUseMcp = this.options.useMcpForOrders ?? (this.isOsirisSigner && (this.options.mcpUrl || this.options.mcpAccessToken));
|
|
209
|
+
if (shouldUseMcp) {
|
|
210
|
+
try {
|
|
211
|
+
this.logger.info("Initializing MCP connection for Osiris signer...");
|
|
212
|
+
const mcpUrl = this.options.mcpUrl;
|
|
213
|
+
const mcpAccessToken = this.options.mcpAccessToken;
|
|
214
|
+
if (!mcpAccessToken) {
|
|
215
|
+
throw new Error("MCP access token is required for MCP mode. Please provide mcpAccessToken in options.");
|
|
216
|
+
}
|
|
217
|
+
this.mcpInstance = await connectMCPServer(mcpUrl, mcpAccessToken);
|
|
218
|
+
this.tradingInitialized = true;
|
|
219
|
+
this.logger.info(`MCP connection initialized for address: ${address}`);
|
|
220
|
+
return;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.logger.error("Failed to initialize MCP connection", error);
|
|
223
|
+
throw new Error(`Failed to initialize MCP connection: ${error.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
this.logger.info("Initializing CLOB trading client...");
|
|
228
|
+
const privateKey = await this.signer.getPrivateKey();
|
|
229
|
+
if (!privateKey) {
|
|
230
|
+
throw new Error("Could not obtain private key for CLOB client initialization. Private key signer is required for trading.");
|
|
231
|
+
}
|
|
232
|
+
this.wallet = new ethers.Wallet(privateKey, this.provider);
|
|
233
|
+
const tempClient = new ClobClient(
|
|
234
|
+
this.options.clobHost,
|
|
235
|
+
this.options.chainId,
|
|
236
|
+
this.wallet
|
|
237
|
+
// Cast to any for CLOB client v5 compatibility
|
|
238
|
+
);
|
|
239
|
+
this.logger.info("Deriving API credentials...");
|
|
240
|
+
const apiCreds = await tempClient.createOrDeriveApiKey();
|
|
241
|
+
const signatureType = 0;
|
|
242
|
+
this.clobClient = new ClobClient(
|
|
243
|
+
this.options.clobHost,
|
|
244
|
+
this.options.chainId,
|
|
245
|
+
this.wallet,
|
|
246
|
+
// Cast to any for CLOB client v5 compatibility
|
|
247
|
+
apiCreds,
|
|
248
|
+
signatureType
|
|
249
|
+
);
|
|
250
|
+
this.tradingInitialized = true;
|
|
251
|
+
this.logger.info(`Trading client initialized for address: ${address}`);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
this.logger.error("Failed to initialize trading client", error);
|
|
254
|
+
throw new Error(`Failed to initialize trading client: ${error.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
isTradingClientInitialized() {
|
|
258
|
+
return this.tradingInitialized;
|
|
259
|
+
}
|
|
260
|
+
async getPrivateKeyFromSigner() {
|
|
261
|
+
if (this.signer && "getPrivateKey" in this.signer) {
|
|
262
|
+
return this.signer.getPrivateKey();
|
|
263
|
+
}
|
|
264
|
+
if (this.signer && this.signer.account?.privateKey) {
|
|
265
|
+
return this.signer.account.privateKey;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
ensureTradingClient() {
|
|
270
|
+
if (!this.clobClient || !this.tradingInitialized) {
|
|
271
|
+
throw new Error("Trading client not initialized. Call initializeTradingClient() first.");
|
|
272
|
+
}
|
|
273
|
+
return this.clobClient;
|
|
274
|
+
}
|
|
275
|
+
ensureMcpInstance() {
|
|
276
|
+
if (!this.mcpInstance || !this.tradingInitialized) {
|
|
277
|
+
throw new Error("MCP connection not initialized. Call initializeTradingClient() first.");
|
|
278
|
+
}
|
|
279
|
+
return this.mcpInstance;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Call an MCP tool with retry logic
|
|
283
|
+
*/
|
|
284
|
+
async callMCPTool(toolName, args) {
|
|
285
|
+
const instance = this.ensureMcpInstance();
|
|
286
|
+
const mcpUrl = instance.url;
|
|
287
|
+
const accessToken = instance.accessToken;
|
|
288
|
+
try {
|
|
289
|
+
return await this.executeMCPTool(instance, toolName, args);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.logger.warning(`Tool execution failed for ${toolName}, reconnecting...`, error instanceof Error ? error.message : error);
|
|
292
|
+
removeConnection(mcpUrl, accessToken);
|
|
293
|
+
this.mcpInstance = await connectMCPServer(mcpUrl, accessToken);
|
|
294
|
+
return await this.executeMCPTool(this.mcpInstance, toolName, args);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Execute a tool on an MCP client instance
|
|
299
|
+
*/
|
|
300
|
+
async executeMCPTool(instance, toolName, args) {
|
|
301
|
+
const client = instance.client;
|
|
302
|
+
if (client._transport && client._transport._client) {
|
|
303
|
+
const mcpClient = client._transport._client;
|
|
304
|
+
const result = await mcpClient.callTool({
|
|
305
|
+
name: toolName,
|
|
306
|
+
arguments: args || {}
|
|
307
|
+
});
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
const tools = await client.tools();
|
|
311
|
+
const tool = tools[toolName];
|
|
312
|
+
if (!tool) {
|
|
313
|
+
throw new Error(`Tool ${toolName} not found on MCP server ${instance.url}`);
|
|
314
|
+
}
|
|
315
|
+
if (typeof tool.execute === "function") {
|
|
316
|
+
const result = await tool.execute(args || {});
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
if (typeof tool === "function") {
|
|
320
|
+
return await tool(args || {});
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`Tool ${toolName} is not callable`);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Extract content from MCP tool response (ported from sozu-tg-bot)
|
|
326
|
+
*/
|
|
327
|
+
extractMCPContent(result) {
|
|
328
|
+
if (typeof result === "string") {
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(result.content)) {
|
|
332
|
+
return result.content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
333
|
+
}
|
|
334
|
+
if (typeof result.text === "string") {
|
|
335
|
+
return result.text;
|
|
336
|
+
}
|
|
337
|
+
if (typeof result.result === "string") {
|
|
338
|
+
return result.result;
|
|
339
|
+
}
|
|
340
|
+
if (result.data) {
|
|
341
|
+
if (typeof result.data === "string") {
|
|
342
|
+
return result.data;
|
|
343
|
+
}
|
|
344
|
+
return JSON.stringify(result.data, null, 2);
|
|
345
|
+
}
|
|
346
|
+
return JSON.stringify(result, null, 2);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Extract order details from MCP response (ported from sozu-tg-bot)
|
|
350
|
+
*/
|
|
351
|
+
extractOrderDetails(resultResponse) {
|
|
352
|
+
const result = this.extractMCPContent(resultResponse);
|
|
353
|
+
let orderResponse = null;
|
|
354
|
+
try {
|
|
355
|
+
orderResponse = JSON.parse(result);
|
|
356
|
+
} catch {
|
|
357
|
+
const codeBlockMatch = result.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/);
|
|
358
|
+
if (codeBlockMatch && codeBlockMatch[1]) {
|
|
359
|
+
try {
|
|
360
|
+
orderResponse = JSON.parse(codeBlockMatch[1]);
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!orderResponse) {
|
|
365
|
+
const responseSectionMatch = result.match(/\*\*Response:\*\*\s*\n?\s*(\{[\s\S]*\})/);
|
|
366
|
+
if (responseSectionMatch && responseSectionMatch[1]) {
|
|
367
|
+
try {
|
|
368
|
+
orderResponse = JSON.parse(responseSectionMatch[1]);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!orderResponse) {
|
|
374
|
+
const anyJsonMatch = result.match(/\{[\s\S]*"success"[\s\S]*\}/);
|
|
375
|
+
if (anyJsonMatch && anyJsonMatch[0]) {
|
|
376
|
+
try {
|
|
377
|
+
orderResponse = JSON.parse(anyJsonMatch[0]);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!orderResponse || typeof orderResponse !== "object") {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
if (!orderResponse.orderID) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const transactionHash = orderResponse.transactionsHashes?.[0] || orderResponse.transactionHash || orderResponse.txHash || orderResponse.hash || "";
|
|
390
|
+
return {
|
|
391
|
+
orderID: orderResponse.orderID,
|
|
392
|
+
transactionHash
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Detect errors in MCP tool response (ported from sozu-tg-bot)
|
|
397
|
+
*/
|
|
398
|
+
detectMCPError(result) {
|
|
399
|
+
if (result && result.isError === true) {
|
|
400
|
+
const errorText = this.extractMCPContent(result);
|
|
401
|
+
return errorText || "Unknown error occurred";
|
|
402
|
+
}
|
|
403
|
+
const content = this.extractMCPContent(result);
|
|
404
|
+
if (!content) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
if (/Error Creating and Posting Order/i.test(content)) {
|
|
408
|
+
const errorMatch = content.match(/\*\*Error Creating and Posting Order\*\*\s*\n\s*(.+?)(?:\n\n|\n*$)/is);
|
|
409
|
+
if (errorMatch && errorMatch[1]) {
|
|
410
|
+
return errorMatch[1].trim();
|
|
411
|
+
}
|
|
412
|
+
const fallbackMatch = content.match(/\*\*Error Creating and Posting Order\*\*[\s\n]+(.+)/is);
|
|
413
|
+
if (fallbackMatch && fallbackMatch[1]) {
|
|
414
|
+
const firstLine = fallbackMatch[1].trim().split("\n")[0];
|
|
415
|
+
return firstLine || "Error creating and posting order";
|
|
416
|
+
}
|
|
417
|
+
return "Error creating and posting order";
|
|
418
|
+
}
|
|
419
|
+
const errorPatterns = [
|
|
420
|
+
{
|
|
421
|
+
pattern: /not enough balance/i,
|
|
422
|
+
extractMessage: () => "Insufficient balance"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
pattern: /not enough allowance/i,
|
|
426
|
+
extractMessage: () => "Insufficient token allowance"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
pattern: /not enough balance.*allowance|not enough allowance.*balance/i,
|
|
430
|
+
extractMessage: () => "Insufficient balance or token allowance"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
pattern: /lower than the minimum/i,
|
|
434
|
+
extractMessage: (content2) => {
|
|
435
|
+
const lines = content2.split("\n");
|
|
436
|
+
const errorLine = lines.find((line) => /lower than the minimum/i.test(line));
|
|
437
|
+
return errorLine ? errorLine.trim() : "Size lower than the minimum";
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
pattern: /higher than the maximum/i,
|
|
442
|
+
extractMessage: (content2) => {
|
|
443
|
+
const lines = content2.split("\n");
|
|
444
|
+
const errorLine = lines.find((line) => /higher than the maximum/i.test(line));
|
|
445
|
+
return errorLine ? errorLine.trim() : "Size higher than the maximum";
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
pattern: /invalid/i,
|
|
450
|
+
extractMessage: (content2) => {
|
|
451
|
+
const lines = content2.split("\n");
|
|
452
|
+
const errorLine = lines.find((line) => /invalid/i.test(line));
|
|
453
|
+
return errorLine ? errorLine.trim() : "Invalid order";
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
pattern: /failed/i,
|
|
458
|
+
extractMessage: (content2) => {
|
|
459
|
+
const lines = content2.split("\n");
|
|
460
|
+
const errorLine = lines.find((line) => /failed/i.test(line));
|
|
461
|
+
return errorLine ? errorLine.trim() : "Order failed";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
];
|
|
465
|
+
for (const { pattern, extractMessage } of errorPatterns) {
|
|
466
|
+
if (pattern.test(content)) {
|
|
467
|
+
return extractMessage(content);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
ensureWallet() {
|
|
473
|
+
if (!this.wallet) {
|
|
474
|
+
throw new Error("Wallet not initialized. Call initializeTradingClient() first.");
|
|
475
|
+
}
|
|
476
|
+
return this.wallet;
|
|
477
|
+
}
|
|
478
|
+
// ============================================
|
|
479
|
+
// Token Approval Methods
|
|
480
|
+
// ============================================
|
|
481
|
+
async getUsdcAllowance() {
|
|
482
|
+
const wallet = this.ensureWallet();
|
|
483
|
+
const usdcContract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, this.provider);
|
|
484
|
+
try {
|
|
485
|
+
const allowance = await usdcContract.allowance(wallet.address, CTF_EXCHANGE_ADDRESS);
|
|
486
|
+
return BigInt(allowance.toString());
|
|
487
|
+
} catch (error) {
|
|
488
|
+
this.logger.error("Error getting USDC allowance", error);
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async approveUsdc(amount) {
|
|
493
|
+
const wallet = this.ensureWallet();
|
|
494
|
+
const usdcContract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, wallet);
|
|
495
|
+
const approvalAmount = amount ?? MaxUint256BigInt;
|
|
496
|
+
try {
|
|
497
|
+
this.logger.info(`Approving USDC for CTF Exchange...`);
|
|
498
|
+
const tx = await usdcContract.approve(CTF_EXCHANGE_ADDRESS, approvalAmount);
|
|
499
|
+
const receipt = await tx.wait();
|
|
500
|
+
this.logger.info(`USDC approval confirmed: ${receipt.hash}`);
|
|
501
|
+
return receipt.hash;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
this.logger.error("Error approving USDC", error);
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async isCtfApprovedForAll() {
|
|
508
|
+
const wallet = this.ensureWallet();
|
|
509
|
+
const ctfContract = new ethers.Contract(CTF_ADDRESS, ERC1155_ABI, this.provider);
|
|
510
|
+
try {
|
|
511
|
+
const isApproved = await ctfContract.isApprovedForAll(wallet.address, CTF_EXCHANGE_ADDRESS);
|
|
512
|
+
return isApproved;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
this.logger.error("Error checking CTF approval", error);
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async approveCtf() {
|
|
519
|
+
const wallet = this.ensureWallet();
|
|
520
|
+
const ctfContract = new ethers.Contract(CTF_ADDRESS, ERC1155_ABI, wallet);
|
|
521
|
+
try {
|
|
522
|
+
this.logger.info(`Approving CTF for CTF Exchange...`);
|
|
523
|
+
const tx = await ctfContract.setApprovalForAll(CTF_EXCHANGE_ADDRESS, true);
|
|
524
|
+
const receipt = await tx.wait();
|
|
525
|
+
this.logger.info(`CTF approval confirmed: ${receipt.hash}`);
|
|
526
|
+
return receipt.hash;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
this.logger.error("Error approving CTF", error);
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async setupApprovals(options) {
|
|
533
|
+
const shouldUseMcp = this.options.useMcpForOrders ?? (this.isOsirisSigner && this.mcpInstance !== null);
|
|
534
|
+
if (shouldUseMcp) {
|
|
535
|
+
return this.setupApprovalsViaMCP();
|
|
536
|
+
}
|
|
537
|
+
const result = {
|
|
538
|
+
usdcWasAlreadyApproved: false,
|
|
539
|
+
ctfWasAlreadyApproved: false
|
|
540
|
+
};
|
|
541
|
+
const currentAllowance = await this.getUsdcAllowance();
|
|
542
|
+
const requiredAllowance = options?.usdcAmount ?? MaxUint256BigInt;
|
|
543
|
+
if (currentAllowance >= requiredAllowance) {
|
|
544
|
+
result.usdcWasAlreadyApproved = true;
|
|
545
|
+
this.logger.info("USDC already approved");
|
|
546
|
+
} else {
|
|
547
|
+
result.usdcApprovalTx = await this.approveUsdc(options?.usdcAmount);
|
|
548
|
+
}
|
|
549
|
+
const ctfApproved = await this.isCtfApprovedForAll();
|
|
550
|
+
if (ctfApproved) {
|
|
551
|
+
result.ctfWasAlreadyApproved = true;
|
|
552
|
+
this.logger.info("CTF already approved");
|
|
553
|
+
} else {
|
|
554
|
+
result.ctfApprovalTx = await this.approveCtf();
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
// ============================================
|
|
559
|
+
// MCP-based Token Approval Methods (for OsirisSigner)
|
|
560
|
+
// ============================================
|
|
561
|
+
/**
|
|
562
|
+
* Check token allowance via MCP tools
|
|
563
|
+
* @param tokenType - "USDC" for buy orders, "CONDITIONAL_TOKEN" for sell orders
|
|
564
|
+
* @returns true if sufficient allowance exists
|
|
565
|
+
*/
|
|
566
|
+
async checkAllowanceViaMCP(tokenType) {
|
|
567
|
+
try {
|
|
568
|
+
const allowanceResult = await this.callMCPTool("check_allowance", {
|
|
569
|
+
token: tokenType
|
|
570
|
+
});
|
|
571
|
+
this.logger.debug("[MCP] Allowance check result:", allowanceResult);
|
|
572
|
+
const allowanceContent = this.extractMCPContent(allowanceResult);
|
|
573
|
+
const contractMatches = allowanceContent?.match(/0x[a-fA-F0-9]{40}:\s*(true|false)/gi);
|
|
574
|
+
let hasFullAllowance = true;
|
|
575
|
+
if (contractMatches && contractMatches.length > 0) {
|
|
576
|
+
hasFullAllowance = contractMatches.every((match) => match.toLowerCase().includes(": true"));
|
|
577
|
+
} else if (allowanceContent) {
|
|
578
|
+
const currentAllowanceMatch = allowanceContent.match(/Current Allowance:\s*(\d+)/i);
|
|
579
|
+
if (currentAllowanceMatch && currentAllowanceMatch[1]) {
|
|
580
|
+
const allowanceValue = parseInt(currentAllowanceMatch[1], 10);
|
|
581
|
+
hasFullAllowance = allowanceValue > 0;
|
|
582
|
+
} else {
|
|
583
|
+
hasFullAllowance = !allowanceContent.toLowerCase().includes(": false") && !allowanceContent.toLowerCase().includes("no allowance") && !allowanceContent.toLowerCase().includes("insufficient") && !allowanceContent.toLowerCase().includes("current allowance: 0");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
this.logger.info(`[MCP] ${tokenType} allowance check: ${hasFullAllowance ? "sufficient" : "insufficient"}`);
|
|
587
|
+
return hasFullAllowance;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
this.logger.error(`[MCP] Error checking ${tokenType} allowance:`, error);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Approve tokens via MCP tools
|
|
595
|
+
* @param tokenType - "USDC" for buy orders, "CONDITIONAL_TOKEN" for sell orders
|
|
596
|
+
* @returns true if approval was successful
|
|
597
|
+
*/
|
|
598
|
+
async approveTokensViaMCP(tokenType) {
|
|
599
|
+
try {
|
|
600
|
+
this.logger.info(`[MCP] Approving ${tokenType}...`);
|
|
601
|
+
const approveResult = await this.callMCPTool("approve_polymarket_tokens", {
|
|
602
|
+
token_type: tokenType === "USDC" ? "USDC" : "CONDITIONAL_TOKENS",
|
|
603
|
+
amount: "100",
|
|
604
|
+
// Amount doesn't matter for max_approval=true
|
|
605
|
+
max_approval: true,
|
|
606
|
+
approve_all_contracts: true,
|
|
607
|
+
chainId: 137
|
|
608
|
+
// Polygon
|
|
609
|
+
});
|
|
610
|
+
const approveContent = this.extractMCPContent(approveResult);
|
|
611
|
+
const mcpError = this.detectMCPError(approveResult);
|
|
612
|
+
if (mcpError) {
|
|
613
|
+
this.logger.error(`[MCP] Failed to approve ${tokenType}: ${mcpError}`);
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const approvalMatch = approveContent?.match(/(\d+)\/(\d+)\s+approvals?\s+successful/i);
|
|
617
|
+
if (approvalMatch && approvalMatch[1] && approvalMatch[2]) {
|
|
618
|
+
const successful = parseInt(approvalMatch[1], 10);
|
|
619
|
+
const total = parseInt(approvalMatch[2], 10);
|
|
620
|
+
const allSuccessful = successful === total;
|
|
621
|
+
if (allSuccessful) {
|
|
622
|
+
this.logger.info(`[MCP] Successfully approved ${tokenType} (${successful}/${total})`);
|
|
623
|
+
} else {
|
|
624
|
+
this.logger.warning(`[MCP] Partial approval for ${tokenType}: ${successful}/${total}`);
|
|
625
|
+
}
|
|
626
|
+
return allSuccessful;
|
|
627
|
+
}
|
|
628
|
+
const hasError = approveContent && (approveContent.toLowerCase().includes("error") || approveContent.toLowerCase().includes("failed"));
|
|
629
|
+
if (hasError) {
|
|
630
|
+
this.logger.error(`[MCP] Failed to approve ${tokenType}:`, approveContent);
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
this.logger.info(`[MCP] ${tokenType} approval completed`);
|
|
634
|
+
return true;
|
|
635
|
+
} catch (error) {
|
|
636
|
+
this.logger.error(`[MCP] Error approving ${tokenType}:`, error);
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Ensure token approval via MCP before placing orders
|
|
642
|
+
* @param side - Order side (BUY or SELL)
|
|
643
|
+
* @returns true if approval is ready
|
|
644
|
+
*/
|
|
645
|
+
async ensureTokenApprovalViaMCP(side) {
|
|
646
|
+
const tokenType = side === "BUY" ? "USDC" : "CONDITIONAL_TOKEN";
|
|
647
|
+
const hasAllowance = await this.checkAllowanceViaMCP(tokenType);
|
|
648
|
+
if (hasAllowance) {
|
|
649
|
+
this.logger.info(`[MCP] ${tokenType} already has sufficient allowance`);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
const approved = await this.approveTokensViaMCP(tokenType);
|
|
653
|
+
return approved;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Setup approvals via MCP tools (for OsirisSigner mode)
|
|
657
|
+
*/
|
|
658
|
+
async setupApprovalsViaMCP() {
|
|
659
|
+
const result = {
|
|
660
|
+
usdcWasAlreadyApproved: false,
|
|
661
|
+
ctfWasAlreadyApproved: false
|
|
662
|
+
};
|
|
663
|
+
try {
|
|
664
|
+
const address = this.userAddress;
|
|
665
|
+
if (!address) {
|
|
666
|
+
throw new Error("userAddress is required for MCP approvals");
|
|
667
|
+
}
|
|
668
|
+
const chooseWalletResult = await this.callMCPTool("choose_wallet", {
|
|
669
|
+
address
|
|
670
|
+
});
|
|
671
|
+
const walletError = this.detectMCPError(chooseWalletResult);
|
|
672
|
+
if (walletError) {
|
|
673
|
+
this.logger.error(`[MCP] Choose wallet failed: ${walletError}`);
|
|
674
|
+
throw new Error(`Choose wallet failed: ${walletError}`);
|
|
675
|
+
}
|
|
676
|
+
const usdcHasAllowance = await this.checkAllowanceViaMCP("USDC");
|
|
677
|
+
if (usdcHasAllowance) {
|
|
678
|
+
result.usdcWasAlreadyApproved = true;
|
|
679
|
+
this.logger.info("[MCP] USDC already approved");
|
|
680
|
+
} else {
|
|
681
|
+
const usdcApproved = await this.approveTokensViaMCP("USDC");
|
|
682
|
+
if (usdcApproved) {
|
|
683
|
+
result.usdcApprovalTx = "mcp-approval-usdc";
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const ctfHasAllowance = await this.checkAllowanceViaMCP("CONDITIONAL_TOKEN");
|
|
687
|
+
if (ctfHasAllowance) {
|
|
688
|
+
result.ctfWasAlreadyApproved = true;
|
|
689
|
+
this.logger.info("[MCP] CTF already approved");
|
|
690
|
+
} else {
|
|
691
|
+
const ctfApproved = await this.approveTokensViaMCP("CONDITIONAL_TOKEN");
|
|
692
|
+
if (ctfApproved) {
|
|
693
|
+
result.ctfApprovalTx = "mcp-approval-ctf";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
this.logger.error("[MCP] Error setting up approvals:", error);
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// ============================================
|
|
703
|
+
// Order Methods
|
|
704
|
+
// ============================================
|
|
705
|
+
async createLimitOrder(params) {
|
|
706
|
+
const shouldUseMcp = this.options.useMcpForOrders ?? (this.isOsirisSigner && this.mcpInstance !== null);
|
|
707
|
+
if (shouldUseMcp) {
|
|
708
|
+
return this.createLimitOrderViaMCP(params);
|
|
709
|
+
}
|
|
710
|
+
const client = this.ensureTradingClient();
|
|
711
|
+
if (this.options.autoApprove) {
|
|
712
|
+
await this.ensureApprovals(params.side);
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
this.logger.info(`Creating limit order: ${params.side} ${params.size} @ ${params.price}`);
|
|
716
|
+
const order = await client.createOrder({
|
|
717
|
+
tokenID: params.tokenId,
|
|
718
|
+
price: params.price,
|
|
719
|
+
side: params.side === "BUY" ? Side.BUY : Side.SELL,
|
|
720
|
+
size: params.size,
|
|
721
|
+
feeRateBps: parseInt(params.feeRateBps ?? "0", 10),
|
|
722
|
+
expiration: params.expiration
|
|
723
|
+
});
|
|
724
|
+
const response = await client.postOrder(order);
|
|
725
|
+
if (response.success) {
|
|
726
|
+
this.logger.info(`Limit order created: ${response.orderID}`);
|
|
727
|
+
return {
|
|
728
|
+
success: true,
|
|
729
|
+
orderId: response.orderID,
|
|
730
|
+
transactionHash: response.transactionsHashes?.[0]
|
|
731
|
+
};
|
|
732
|
+
} else {
|
|
733
|
+
this.logger.error(`Limit order failed: ${response.errorMsg}`);
|
|
734
|
+
return {
|
|
735
|
+
success: false,
|
|
736
|
+
errorMsg: response.errorMsg
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
this.logger.error("Error creating limit order", error);
|
|
741
|
+
return {
|
|
742
|
+
success: false,
|
|
743
|
+
errorMsg: error.message
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Create limit order via MCP tools (for OsirisSigner)
|
|
749
|
+
*/
|
|
750
|
+
async createLimitOrderViaMCP(params) {
|
|
751
|
+
try {
|
|
752
|
+
const address = this.userAddress;
|
|
753
|
+
if (!address) {
|
|
754
|
+
throw new Error("userAddress is required for MCP order placement");
|
|
755
|
+
}
|
|
756
|
+
this.logger.info(`[MCP] Creating limit order: ${params.side} ${params.size} @ ${params.price}`);
|
|
757
|
+
this.logger.debug(`[MCP] Using wallet address: ${address}`);
|
|
758
|
+
this.logger.debug(`[MCP] Calling choose_wallet for address: ${address}`);
|
|
759
|
+
const chooseWalletResult = await this.callMCPTool("choose_wallet", {
|
|
760
|
+
address
|
|
761
|
+
});
|
|
762
|
+
this.logger.debug(`[MCP] choose_wallet result: ${JSON.stringify(chooseWalletResult, null, 2)}`);
|
|
763
|
+
const walletError = this.detectMCPError(chooseWalletResult);
|
|
764
|
+
if (walletError) {
|
|
765
|
+
this.logger.error(`[MCP] Choose wallet failed: ${walletError}`);
|
|
766
|
+
return {
|
|
767
|
+
success: false,
|
|
768
|
+
errorMsg: walletError
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const approvalSuccess = await this.ensureTokenApprovalViaMCP(params.side);
|
|
772
|
+
if (!approvalSuccess) {
|
|
773
|
+
return {
|
|
774
|
+
success: false,
|
|
775
|
+
errorMsg: `Failed to approve ${params.side === "BUY" ? "USDC" : "Conditional Token"}. Please try again.`
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const orderParams = {
|
|
779
|
+
userOrder: {
|
|
780
|
+
tokenID: params.tokenId,
|
|
781
|
+
side: params.side,
|
|
782
|
+
size: params.size,
|
|
783
|
+
price: params.price
|
|
784
|
+
},
|
|
785
|
+
orderType: params.expiration ? "GTD" : "GTC",
|
|
786
|
+
deferExec: false
|
|
787
|
+
};
|
|
788
|
+
this.logger.debug(`[MCP] Limit order params:`, orderParams);
|
|
789
|
+
this.logger.info(`[MCP] Submitting limit order: ${params.side} ${params.size} @ ${params.price} for token ${params.tokenId.substring(0, 20)}...`);
|
|
790
|
+
const orderResult = await this.callMCPTool("create_and_post_order", orderParams);
|
|
791
|
+
this.logger.debug(`[MCP] Raw limit order result: ${JSON.stringify(orderResult, null, 2)}`);
|
|
792
|
+
const orderError = this.detectMCPError(orderResult);
|
|
793
|
+
if (orderError) {
|
|
794
|
+
this.logger.error(`Limit order via MCP failed: ${orderError}`);
|
|
795
|
+
return {
|
|
796
|
+
success: false,
|
|
797
|
+
errorMsg: orderError
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const orderDetails = this.extractOrderDetails(orderResult);
|
|
801
|
+
if (!orderDetails) {
|
|
802
|
+
this.logger.error(`Failed to extract order details from MCP response`, orderResult);
|
|
803
|
+
return {
|
|
804
|
+
success: false,
|
|
805
|
+
errorMsg: "Failed to extract order details from MCP response"
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
this.logger.info(`[MCP] Limit order created: ${orderDetails.orderID}`);
|
|
809
|
+
return {
|
|
810
|
+
success: true,
|
|
811
|
+
orderId: orderDetails.orderID,
|
|
812
|
+
transactionHash: orderDetails.transactionHash
|
|
813
|
+
};
|
|
814
|
+
} catch (error) {
|
|
815
|
+
this.logger.error("Error creating limit order via MCP", error);
|
|
816
|
+
return {
|
|
817
|
+
success: false,
|
|
818
|
+
errorMsg: error.message || "Unknown error"
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async createMarketOrder(params) {
|
|
823
|
+
const shouldUseMcp = this.options.useMcpForOrders ?? (this.isOsirisSigner && this.mcpInstance !== null);
|
|
824
|
+
if (shouldUseMcp) {
|
|
825
|
+
return this.createMarketOrderViaMCP(params);
|
|
826
|
+
}
|
|
827
|
+
const client = this.ensureTradingClient();
|
|
828
|
+
if (this.options.autoApprove) {
|
|
829
|
+
await this.ensureApprovals(params.side);
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
this.logger.info(`Creating market order: ${params.side} ${params.amount}`);
|
|
833
|
+
const response = await client.createAndPostMarketOrder({
|
|
834
|
+
tokenID: params.tokenId,
|
|
835
|
+
amount: params.amount,
|
|
836
|
+
side: params.side === "BUY" ? Side.BUY : Side.SELL
|
|
837
|
+
});
|
|
838
|
+
if (response.success) {
|
|
839
|
+
this.logger.info(`Market order executed: ${response.orderID}`);
|
|
840
|
+
return {
|
|
841
|
+
success: true,
|
|
842
|
+
orderId: response.orderID,
|
|
843
|
+
transactionHash: response.transactionsHashes?.[0]
|
|
844
|
+
};
|
|
845
|
+
} else {
|
|
846
|
+
this.logger.error(`Market order failed: ${response.errorMsg}`);
|
|
847
|
+
return {
|
|
848
|
+
success: false,
|
|
849
|
+
errorMsg: response.errorMsg
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
} catch (error) {
|
|
853
|
+
this.logger.error("Error creating market order", error);
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
errorMsg: error.message
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Create market order via MCP tools (for OsirisSigner)
|
|
862
|
+
*/
|
|
863
|
+
async createMarketOrderViaMCP(params) {
|
|
864
|
+
try {
|
|
865
|
+
const address = this.userAddress;
|
|
866
|
+
if (!address) {
|
|
867
|
+
throw new Error("userAddress is required for MCP order placement");
|
|
868
|
+
}
|
|
869
|
+
this.logger.info(`[MCP] Creating market order: ${params.side} ${params.amount} USDC`);
|
|
870
|
+
this.logger.debug(`[MCP] Using wallet address: ${address}`);
|
|
871
|
+
this.logger.debug(`[MCP] Order details: tokenId=${params.tokenId}, side=${params.side}, amount=${params.amount}`);
|
|
872
|
+
this.logger.debug(`[MCP] Calling choose_wallet for address: ${address}`);
|
|
873
|
+
const chooseWalletResult = await this.callMCPTool("choose_wallet", {
|
|
874
|
+
address
|
|
875
|
+
});
|
|
876
|
+
this.logger.debug(`[MCP] choose_wallet result: ${JSON.stringify(chooseWalletResult, null, 2)}`);
|
|
877
|
+
const walletError = this.detectMCPError(chooseWalletResult);
|
|
878
|
+
if (walletError) {
|
|
879
|
+
this.logger.error(`Choose wallet failed: ${walletError}`);
|
|
880
|
+
return {
|
|
881
|
+
success: false,
|
|
882
|
+
errorMsg: walletError
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const approvalSuccess = await this.ensureTokenApprovalViaMCP(params.side);
|
|
886
|
+
if (!approvalSuccess) {
|
|
887
|
+
return {
|
|
888
|
+
success: false,
|
|
889
|
+
errorMsg: `Failed to approve ${params.side === "BUY" ? "USDC" : "Conditional Token"}. Please try again.`
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const orderParams = {
|
|
893
|
+
userMarketOrder: {
|
|
894
|
+
tokenID: params.tokenId,
|
|
895
|
+
side: params.side,
|
|
896
|
+
amount: params.amount
|
|
897
|
+
},
|
|
898
|
+
orderType: "FOK"
|
|
899
|
+
// Fill or Kill
|
|
900
|
+
};
|
|
901
|
+
this.logger.debug(`[MCP] Market order params:`, orderParams);
|
|
902
|
+
this.logger.info(`[MCP] Submitting market order: ${params.side} ${params.amount} USDC for token ${params.tokenId.substring(0, 20)}...`);
|
|
903
|
+
const orderResult = await this.callMCPTool("create_and_post_market_order", orderParams);
|
|
904
|
+
this.logger.debug(`[MCP] Raw order result: ${JSON.stringify(orderResult, null, 2)}`);
|
|
905
|
+
const orderError = this.detectMCPError(orderResult);
|
|
906
|
+
if (orderError) {
|
|
907
|
+
this.logger.error(`Market order via MCP failed: ${orderError}`);
|
|
908
|
+
return {
|
|
909
|
+
success: false,
|
|
910
|
+
errorMsg: orderError
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const orderDetails = this.extractOrderDetails(orderResult);
|
|
914
|
+
if (!orderDetails) {
|
|
915
|
+
this.logger.error(`Failed to extract order details from MCP response`, orderResult);
|
|
916
|
+
return {
|
|
917
|
+
success: false,
|
|
918
|
+
errorMsg: "Failed to extract order details from MCP response"
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
this.logger.info(`Market order via MCP executed: ${orderDetails.orderID}`);
|
|
922
|
+
return {
|
|
923
|
+
success: true,
|
|
924
|
+
orderId: orderDetails.orderID,
|
|
925
|
+
transactionHash: orderDetails.transactionHash
|
|
926
|
+
};
|
|
927
|
+
} catch (error) {
|
|
928
|
+
this.logger.error("Error creating market order via MCP", error);
|
|
929
|
+
return {
|
|
930
|
+
success: false,
|
|
931
|
+
errorMsg: error.message || "Unknown error"
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async getOpenOrders() {
|
|
936
|
+
const client = this.ensureTradingClient();
|
|
937
|
+
try {
|
|
938
|
+
const orders = await client.getOpenOrders();
|
|
939
|
+
return orders.map((order) => ({
|
|
940
|
+
id: order.id || order.order_id,
|
|
941
|
+
market: order.market,
|
|
942
|
+
asset_id: order.asset_id,
|
|
943
|
+
side: order.side,
|
|
944
|
+
original_size: order.original_size || order.size,
|
|
945
|
+
size_matched: order.size_matched || "0",
|
|
946
|
+
price: order.price,
|
|
947
|
+
outcome: order.outcome,
|
|
948
|
+
owner: order.owner,
|
|
949
|
+
timestamp: order.timestamp || Date.now(),
|
|
950
|
+
expiration: order.expiration || 0,
|
|
951
|
+
order_type: order.order_type || "LIMIT"
|
|
952
|
+
}));
|
|
953
|
+
} catch (error) {
|
|
954
|
+
this.logger.error("Error getting open orders", error);
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async cancelOrder(orderId) {
|
|
959
|
+
const client = this.ensureTradingClient();
|
|
960
|
+
try {
|
|
961
|
+
this.logger.info(`Canceling order: ${orderId}`);
|
|
962
|
+
const response = await client.cancelOrder({ orderID: orderId });
|
|
963
|
+
return {
|
|
964
|
+
canceled: response.canceled || [],
|
|
965
|
+
not_canceled: response.not_canceled || {}
|
|
966
|
+
};
|
|
967
|
+
} catch (error) {
|
|
968
|
+
this.logger.error("Error canceling order", error);
|
|
969
|
+
throw error;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async cancelOrders(orderIds) {
|
|
973
|
+
const client = this.ensureTradingClient();
|
|
974
|
+
try {
|
|
975
|
+
this.logger.info(`Canceling ${orderIds.length} orders`);
|
|
976
|
+
const response = await client.cancelOrders(orderIds);
|
|
977
|
+
return {
|
|
978
|
+
canceled: response.canceled || [],
|
|
979
|
+
not_canceled: response.not_canceled || {}
|
|
980
|
+
};
|
|
981
|
+
} catch (error) {
|
|
982
|
+
this.logger.error("Error canceling orders", error);
|
|
983
|
+
throw error;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async cancelAllOrders() {
|
|
987
|
+
const client = this.ensureTradingClient();
|
|
988
|
+
try {
|
|
989
|
+
this.logger.info("Canceling all orders");
|
|
990
|
+
const response = await client.cancelAll();
|
|
991
|
+
return {
|
|
992
|
+
canceled: response.canceled || [],
|
|
993
|
+
not_canceled: response.not_canceled || {}
|
|
994
|
+
};
|
|
995
|
+
} catch (error) {
|
|
996
|
+
this.logger.error("Error canceling all orders", error);
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async getOrderBook(tokenId) {
|
|
1001
|
+
const client = this.ensureTradingClient();
|
|
1002
|
+
try {
|
|
1003
|
+
const book = await client.getOrderBook(tokenId);
|
|
1004
|
+
return book;
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
this.logger.error("Error getting order book", error);
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async ensureApprovals(side) {
|
|
1011
|
+
if (side === "BUY") {
|
|
1012
|
+
const allowance = await this.getUsdcAllowance();
|
|
1013
|
+
if (allowance === BigInt(0)) {
|
|
1014
|
+
this.logger.info("Auto-approving USDC...");
|
|
1015
|
+
await this.approveUsdc();
|
|
1016
|
+
}
|
|
1017
|
+
} else {
|
|
1018
|
+
const approved = await this.isCtfApprovedForAll();
|
|
1019
|
+
if (!approved) {
|
|
1020
|
+
this.logger.info("Auto-approving CTF...");
|
|
1021
|
+
await this.approveCtf();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// ============================================
|
|
1026
|
+
// Legacy Execution Methods (backward compatible)
|
|
1027
|
+
// ============================================
|
|
1028
|
+
async buy(tokenId, size) {
|
|
1029
|
+
if (!this.tradingInitialized) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`Trading client not initialized. Call initializeTradingClient() before placing orders. Attempted to buy ${size} USDC of token ${tokenId}.`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
const response = await this.createMarketOrder({
|
|
1035
|
+
tokenId,
|
|
1036
|
+
amount: size,
|
|
1037
|
+
side: "BUY"
|
|
1038
|
+
});
|
|
1039
|
+
if (!response.success) {
|
|
1040
|
+
throw new Error(`Buy order failed: ${response.errorMsg}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async sell(tokenId, size) {
|
|
1044
|
+
if (!this.tradingInitialized) {
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
`Trading client not initialized. Call initializeTradingClient() before placing orders. Attempted to sell ${size} shares of token ${tokenId}.`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
const response = await this.createMarketOrder({
|
|
1050
|
+
tokenId,
|
|
1051
|
+
amount: size,
|
|
1052
|
+
side: "SELL"
|
|
1053
|
+
});
|
|
1054
|
+
if (!response.success) {
|
|
1055
|
+
throw new Error(`Sell order failed: ${response.errorMsg}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// ============================================
|
|
1059
|
+
// Convenience Trading Methods
|
|
1060
|
+
// ============================================
|
|
1061
|
+
/**
|
|
1062
|
+
* Place a limit buy order
|
|
1063
|
+
*/
|
|
1064
|
+
async buyLimit(tokenId, price, size) {
|
|
1065
|
+
if (!this.tradingInitialized) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return this.createLimitOrder({
|
|
1071
|
+
tokenId,
|
|
1072
|
+
price,
|
|
1073
|
+
size,
|
|
1074
|
+
side: "BUY"
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Place a limit sell order
|
|
1079
|
+
*/
|
|
1080
|
+
async sellLimit(tokenId, price, size) {
|
|
1081
|
+
if (!this.tradingInitialized) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return this.createLimitOrder({
|
|
1087
|
+
tokenId,
|
|
1088
|
+
price,
|
|
1089
|
+
size,
|
|
1090
|
+
side: "SELL"
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Place a market buy order
|
|
1095
|
+
*/
|
|
1096
|
+
async buyMarket(tokenId, amount, slippage = 0.05) {
|
|
1097
|
+
if (!this.tradingInitialized) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
return this.createMarketOrder({
|
|
1103
|
+
tokenId,
|
|
1104
|
+
amount,
|
|
1105
|
+
side: "BUY",
|
|
1106
|
+
slippageTolerance: slippage
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Place a market sell order
|
|
1111
|
+
*/
|
|
1112
|
+
async sellMarket(tokenId, size, slippage = 0.05) {
|
|
1113
|
+
if (!this.tradingInitialized) {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
`Trading client not initialized. Call initializeTradingClient() before placing orders.`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
return this.createMarketOrder({
|
|
1119
|
+
tokenId,
|
|
1120
|
+
amount: size,
|
|
1121
|
+
side: "SELL",
|
|
1122
|
+
slippageTolerance: slippage
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
// ============================================
|
|
1126
|
+
// Data Methods
|
|
1127
|
+
// ============================================
|
|
1128
|
+
async getPrice(ticker) {
|
|
1129
|
+
try {
|
|
1130
|
+
const market = await this.gammaClient.getMarket(ticker);
|
|
1131
|
+
if (market.last_trade_price) {
|
|
1132
|
+
return parseFloat(market.last_trade_price);
|
|
1133
|
+
}
|
|
1134
|
+
if (market.best_bid && market.best_ask) {
|
|
1135
|
+
return (parseFloat(market.best_bid) + parseFloat(market.best_ask)) / 2;
|
|
1136
|
+
}
|
|
1137
|
+
return 0;
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
this.logger.error(`Error fetching price for ${ticker}`, error);
|
|
1140
|
+
return 0;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async getIndicator(ticker, indicator, params) {
|
|
1144
|
+
this.logger.warning(`getIndicator not implemented for Polymarket`);
|
|
1145
|
+
return 0;
|
|
1146
|
+
}
|
|
1147
|
+
// ============================================
|
|
1148
|
+
// Info Methods
|
|
1149
|
+
// ============================================
|
|
1150
|
+
async search(params) {
|
|
1151
|
+
return this.gammaClient.search(params);
|
|
1152
|
+
}
|
|
1153
|
+
async getMarket(idOrSlug) {
|
|
1154
|
+
return this.gammaClient.getMarket(idOrSlug);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Get markets from Polymarket Gamma API with full filter support
|
|
1158
|
+
* https://docs.polymarket.com/api-reference/markets/list-markets
|
|
1159
|
+
*/
|
|
1160
|
+
async getMarkets(filters) {
|
|
1161
|
+
const GAMMA_API_BASE = "https://gamma-api.polymarket.com";
|
|
1162
|
+
const url = new URL(`${GAMMA_API_BASE}/markets`);
|
|
1163
|
+
if (filters) {
|
|
1164
|
+
if (filters.active !== void 0) url.searchParams.append("active", String(filters.active));
|
|
1165
|
+
if (filters.closed !== void 0) url.searchParams.append("closed", String(filters.closed));
|
|
1166
|
+
if (filters.end_date_min) url.searchParams.append("end_date_min", filters.end_date_min);
|
|
1167
|
+
if (filters.end_date_max) url.searchParams.append("end_date_max", filters.end_date_max);
|
|
1168
|
+
if (filters.start_date_min) url.searchParams.append("start_date_min", filters.start_date_min);
|
|
1169
|
+
if (filters.start_date_max) url.searchParams.append("start_date_max", filters.start_date_max);
|
|
1170
|
+
if (filters.liquidity_num_min !== void 0) url.searchParams.append("liquidity_num_min", String(filters.liquidity_num_min));
|
|
1171
|
+
if (filters.liquidity_num_max !== void 0) url.searchParams.append("liquidity_num_max", String(filters.liquidity_num_max));
|
|
1172
|
+
if (filters.volume_num_min !== void 0) url.searchParams.append("volume_num_min", String(filters.volume_num_min));
|
|
1173
|
+
if (filters.volume_num_max !== void 0) url.searchParams.append("volume_num_max", String(filters.volume_num_max));
|
|
1174
|
+
if (filters.limit !== void 0) url.searchParams.append("limit", String(filters.limit));
|
|
1175
|
+
if (filters.offset !== void 0) url.searchParams.append("offset", String(filters.offset));
|
|
1176
|
+
if (filters.order) url.searchParams.append("order", filters.order);
|
|
1177
|
+
if (filters.ascending !== void 0) url.searchParams.append("ascending", String(filters.ascending));
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
const response = await fetch(url.toString());
|
|
1181
|
+
if (!response.ok) {
|
|
1182
|
+
throw new Error(`Gamma API error: ${response.status} ${response.statusText}`);
|
|
1183
|
+
}
|
|
1184
|
+
const data = await response.json();
|
|
1185
|
+
return data;
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
this.logger.error("Error fetching markets from Gamma API", error);
|
|
1188
|
+
throw error;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async getMarketTags(marketId) {
|
|
1192
|
+
return this.gammaClient.getMarketTags(marketId);
|
|
1193
|
+
}
|
|
1194
|
+
async getEvent(idOrSlug) {
|
|
1195
|
+
return this.gammaClient.getEvent(idOrSlug);
|
|
1196
|
+
}
|
|
1197
|
+
async getEvents(filters) {
|
|
1198
|
+
return this.gammaClient.getEvents(filters);
|
|
1199
|
+
}
|
|
1200
|
+
async getEventTags(eventId) {
|
|
1201
|
+
return this.gammaClient.getEventTags(eventId);
|
|
1202
|
+
}
|
|
1203
|
+
async getEventsPaginated(filters) {
|
|
1204
|
+
return this.gammaClient.getEventsPaginated(filters);
|
|
1205
|
+
}
|
|
1206
|
+
async getTags(params) {
|
|
1207
|
+
return this.gammaClient.getTags(params);
|
|
1208
|
+
}
|
|
1209
|
+
async getTag(idOrSlug) {
|
|
1210
|
+
return this.gammaClient.getTag(idOrSlug);
|
|
1211
|
+
}
|
|
1212
|
+
async getRelatedTags(idOrSlug) {
|
|
1213
|
+
return this.gammaClient.getRelatedTags(idOrSlug);
|
|
1214
|
+
}
|
|
1215
|
+
async getRelatedTagsTags(idOrSlug) {
|
|
1216
|
+
return this.gammaClient.getRelatedTagsTags(idOrSlug);
|
|
1217
|
+
}
|
|
1218
|
+
async getTeams(params) {
|
|
1219
|
+
return this.gammaClient.getTeams(params);
|
|
1220
|
+
}
|
|
1221
|
+
async getSports(params) {
|
|
1222
|
+
return this.gammaClient.getSports(params);
|
|
1223
|
+
}
|
|
1224
|
+
async getSeries(params) {
|
|
1225
|
+
return this.gammaClient.getSeries(params);
|
|
1226
|
+
}
|
|
1227
|
+
async getSeriesById(seriesId) {
|
|
1228
|
+
return this.gammaClient.getSeriesById(seriesId);
|
|
1229
|
+
}
|
|
1230
|
+
async getComments(params) {
|
|
1231
|
+
return this.gammaClient.getComments(params);
|
|
1232
|
+
}
|
|
1233
|
+
async getComment(commentId) {
|
|
1234
|
+
return this.gammaClient.getComment(commentId);
|
|
1235
|
+
}
|
|
1236
|
+
async getCommentsByUser(userAddress, params) {
|
|
1237
|
+
return this.gammaClient.getCommentsByUser(userAddress, params);
|
|
1238
|
+
}
|
|
1239
|
+
// ============================================
|
|
1240
|
+
// Data/CLOB API Methods
|
|
1241
|
+
// ============================================
|
|
1242
|
+
DATA_API_BASE = "https://data-api.polymarket.com";
|
|
1243
|
+
async fetchData(endpoint, params) {
|
|
1244
|
+
const url = new URL(`${this.DATA_API_BASE}${endpoint}`);
|
|
1245
|
+
if (params) {
|
|
1246
|
+
Object.keys(params).forEach((key) => {
|
|
1247
|
+
if (params[key] !== void 0) {
|
|
1248
|
+
url.searchParams.append(key, String(params[key]));
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
const response = await fetch(url.toString());
|
|
1254
|
+
if (!response.ok) {
|
|
1255
|
+
throw new Error(`Data API error: ${response.status} ${response.statusText}`);
|
|
1256
|
+
}
|
|
1257
|
+
return await response.json();
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
this.logger.error(`Error fetching data from ${endpoint}`, error);
|
|
1260
|
+
throw error;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async getPositions(user, params) {
|
|
1264
|
+
return this.fetchData("/positions", { user, ...params });
|
|
1265
|
+
}
|
|
1266
|
+
async getTrades(user, params) {
|
|
1267
|
+
return this.fetchData("/trades", { user, ...params });
|
|
1268
|
+
}
|
|
1269
|
+
async getActivity(user, params) {
|
|
1270
|
+
return this.fetchData("/activity", { user, ...params });
|
|
1271
|
+
}
|
|
1272
|
+
async getTopHolders(marketId) {
|
|
1273
|
+
try {
|
|
1274
|
+
if (this.clobClient) {
|
|
1275
|
+
try {
|
|
1276
|
+
const market = await this.getMarket(marketId);
|
|
1277
|
+
if (market && market.tokens) {
|
|
1278
|
+
return market.tokens.map((token) => ({
|
|
1279
|
+
tokenId: token.token_id,
|
|
1280
|
+
outcome: token.outcome,
|
|
1281
|
+
holders: token.holders || 0
|
|
1282
|
+
}));
|
|
1283
|
+
}
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
this.logger.debug("getTopHolders: No direct API available, returning market token info if available");
|
|
1288
|
+
return [];
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
this.logger.error(`Error fetching top holders for ${marketId}`, error);
|
|
1291
|
+
return [];
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async getPortfolioValue(user) {
|
|
1295
|
+
try {
|
|
1296
|
+
const positions = await this.getPositions(user);
|
|
1297
|
+
return positions.reduce((total, pos) => {
|
|
1298
|
+
const value = pos.currentValue || pos.size * pos.price || 0;
|
|
1299
|
+
return total + value;
|
|
1300
|
+
}, 0);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
this.logger.error("Error calculating portfolio value", error);
|
|
1303
|
+
return 0;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async getClosedPositions(user, params) {
|
|
1307
|
+
try {
|
|
1308
|
+
const allPositions = await this.getPositions(user, params);
|
|
1309
|
+
const closedPositions = allPositions.filter((pos) => {
|
|
1310
|
+
const size = parseFloat(pos.size || pos.amount || "0");
|
|
1311
|
+
const isClosed = size === 0 || pos.closed === true || pos.status === "closed";
|
|
1312
|
+
return isClosed;
|
|
1313
|
+
});
|
|
1314
|
+
return closedPositions;
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
this.logger.error("Error fetching closed positions", error);
|
|
1317
|
+
return [];
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async getTradedMarketsCount(user) {
|
|
1321
|
+
try {
|
|
1322
|
+
const trades = await this.getTrades(user, { limit: 1e3 });
|
|
1323
|
+
const marketIds = new Set(trades.map((t) => t.market));
|
|
1324
|
+
return marketIds.size;
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
this.logger.error("Error counting traded markets", error);
|
|
1327
|
+
return 0;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async getOpenInterest(marketId) {
|
|
1331
|
+
try {
|
|
1332
|
+
const market = await this.getMarket(marketId);
|
|
1333
|
+
return market.open_interest ? parseFloat(market.open_interest) : 0;
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
this.logger.error(`Error fetching open interest for ${marketId}`, error);
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
// Account methods
|
|
1340
|
+
async getPosition(ticker) {
|
|
1341
|
+
try {
|
|
1342
|
+
if (!this.userAddress) {
|
|
1343
|
+
this.logger.error("getPosition requires userAddress to be set");
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
const positions = await this.getPositions(this.userAddress);
|
|
1347
|
+
const matchingPosition = positions.find((pos) => {
|
|
1348
|
+
const marketMatch = pos.market === ticker || pos.slug === ticker || pos.condition_id === ticker || pos.asset_id === ticker || pos.token_id === ticker;
|
|
1349
|
+
return marketMatch && parseFloat(pos.size || pos.amount || "0") > 0;
|
|
1350
|
+
});
|
|
1351
|
+
return matchingPosition || null;
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
this.logger.error(`Error fetching position for ${ticker}`, error);
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
var HyperliquidClient = class {
|
|
1359
|
+
constructor(logger, userAddress) {
|
|
1360
|
+
this.logger = logger;
|
|
1361
|
+
this.userAddress = userAddress || null;
|
|
1362
|
+
this.client = new Hyperliquid({
|
|
1363
|
+
enableWs: false,
|
|
1364
|
+
walletAddress: userAddress,
|
|
1365
|
+
testnet: false,
|
|
1366
|
+
disableAssetMapRefresh: true
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
client;
|
|
1370
|
+
userAddress;
|
|
1371
|
+
// Data methods
|
|
1372
|
+
async getPrice(ticker) {
|
|
1373
|
+
try {
|
|
1374
|
+
const mids = await this.getAllMids();
|
|
1375
|
+
const normalizedTicker = ticker.toUpperCase();
|
|
1376
|
+
const price = mids[normalizedTicker];
|
|
1377
|
+
if (price) {
|
|
1378
|
+
return parseFloat(price);
|
|
1379
|
+
}
|
|
1380
|
+
this.logger.warning(`Price not found for ${ticker} on Hyperliquid`);
|
|
1381
|
+
return 0;
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
this.logger.error(`Error fetching price for ${ticker}`, error);
|
|
1384
|
+
return 0;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async getIndicator(ticker, indicator, params) {
|
|
1388
|
+
this.logger.warning(`getIndicator not implemented for Hyperliquid`);
|
|
1389
|
+
return 0;
|
|
1390
|
+
}
|
|
1391
|
+
// Spot Info
|
|
1392
|
+
async getSpotMeta() {
|
|
1393
|
+
return this.client.info.spot.getSpotMeta();
|
|
1394
|
+
}
|
|
1395
|
+
async getSpotClearinghouseState(user) {
|
|
1396
|
+
return this.client.info.spot.getSpotClearinghouseState(user);
|
|
1397
|
+
}
|
|
1398
|
+
async getSpotMetaAndAssetCtxs() {
|
|
1399
|
+
return this.client.info.spot.getSpotMetaAndAssetCtxs();
|
|
1400
|
+
}
|
|
1401
|
+
async getTokenDetails(tokenId) {
|
|
1402
|
+
return this.client.info.spot.getTokenDetails(tokenId);
|
|
1403
|
+
}
|
|
1404
|
+
async getSpotDeployState(user) {
|
|
1405
|
+
return this.client.info.spot.getSpotDeployState(user);
|
|
1406
|
+
}
|
|
1407
|
+
// Perpetuals Info
|
|
1408
|
+
async getMeta() {
|
|
1409
|
+
return this.client.info.perpetuals.getMeta();
|
|
1410
|
+
}
|
|
1411
|
+
async getMetaAndAssetCtxs() {
|
|
1412
|
+
return this.client.info.perpetuals.getMetaAndAssetCtxs();
|
|
1413
|
+
}
|
|
1414
|
+
async getClearinghouseState(user) {
|
|
1415
|
+
return this.client.info.perpetuals.getClearinghouseState(user);
|
|
1416
|
+
}
|
|
1417
|
+
async getUserFunding(user, startTime, endTime) {
|
|
1418
|
+
return this.client.info.perpetuals.getUserFunding(user, startTime, endTime);
|
|
1419
|
+
}
|
|
1420
|
+
async getUserNonFundingLedgerUpdates(user, startTime, endTime) {
|
|
1421
|
+
return this.client.info.perpetuals.getUserNonFundingLedgerUpdates(user, startTime, endTime);
|
|
1422
|
+
}
|
|
1423
|
+
async getFundingHistory(coin, startTime, endTime) {
|
|
1424
|
+
return this.client.info.perpetuals.getFundingHistory(coin, startTime, endTime);
|
|
1425
|
+
}
|
|
1426
|
+
async getPredictedFundings() {
|
|
1427
|
+
return this.client.info.perpetuals.getPredictedFundings();
|
|
1428
|
+
}
|
|
1429
|
+
async getPerpsAtOpenInterestCap() {
|
|
1430
|
+
return this.client.info.perpetuals.getPerpsAtOpenInterestCap();
|
|
1431
|
+
}
|
|
1432
|
+
async getPerpDexLimits(dex) {
|
|
1433
|
+
return this.client.info.perpetuals.getPerpDexLimits(dex);
|
|
1434
|
+
}
|
|
1435
|
+
// General Info
|
|
1436
|
+
async getAssetIndex(assetName) {
|
|
1437
|
+
return this.client.info.getAssetIndex(assetName);
|
|
1438
|
+
}
|
|
1439
|
+
async getInternalName(exchangeName) {
|
|
1440
|
+
return this.client.info.getInternalName(exchangeName);
|
|
1441
|
+
}
|
|
1442
|
+
async getAllAssets() {
|
|
1443
|
+
return this.client.info.getAllAssets();
|
|
1444
|
+
}
|
|
1445
|
+
async getAllMids() {
|
|
1446
|
+
const mids = await this.client.info.getAllMids();
|
|
1447
|
+
return Object.fromEntries(Object.entries(mids).map(([key, value]) => [key.replace("-PERP", "").replace("-SPOT", ""), value]));
|
|
1448
|
+
}
|
|
1449
|
+
async getUserOpenOrders(user) {
|
|
1450
|
+
return this.client.info.getUserOpenOrders(user);
|
|
1451
|
+
}
|
|
1452
|
+
async getFrontendOpenOrders(user) {
|
|
1453
|
+
return this.client.info.getFrontendOpenOrders(user);
|
|
1454
|
+
}
|
|
1455
|
+
async getUserFills(user) {
|
|
1456
|
+
return this.client.info.getUserFills(user);
|
|
1457
|
+
}
|
|
1458
|
+
async getUserFillsByTime(user, startTime, endTime) {
|
|
1459
|
+
return this.client.info.getUserFillsByTime(user, startTime, endTime);
|
|
1460
|
+
}
|
|
1461
|
+
async getUserRateLimit(user) {
|
|
1462
|
+
return this.client.info.getUserRateLimit(user);
|
|
1463
|
+
}
|
|
1464
|
+
async getOrderStatus(user, oid) {
|
|
1465
|
+
return this.client.info.getOrderStatus(user, oid);
|
|
1466
|
+
}
|
|
1467
|
+
async getL2Book(coin) {
|
|
1468
|
+
return this.client.info.getL2Book(coin);
|
|
1469
|
+
}
|
|
1470
|
+
async getCandleSnapshot(coin, interval, startTime, endTime) {
|
|
1471
|
+
return this.client.info.getCandleSnapshot(coin, interval, startTime, endTime);
|
|
1472
|
+
}
|
|
1473
|
+
async getMaxBuilderFee(user, builder) {
|
|
1474
|
+
return this.client.info.getMaxBuilderFee(user, builder);
|
|
1475
|
+
}
|
|
1476
|
+
async getHistoricalOrders(user) {
|
|
1477
|
+
return this.client.info.getHistoricalOrders(user);
|
|
1478
|
+
}
|
|
1479
|
+
async getUserTwapSliceFills(user) {
|
|
1480
|
+
return this.client.info.getUserTwapSliceFills(user);
|
|
1481
|
+
}
|
|
1482
|
+
async getSubAccounts(user) {
|
|
1483
|
+
return this.client.info.getSubAccounts(user);
|
|
1484
|
+
}
|
|
1485
|
+
async getVaultDetails(vaultAddress, user) {
|
|
1486
|
+
return this.client.info.getVaultDetails(vaultAddress, user);
|
|
1487
|
+
}
|
|
1488
|
+
async getUserVaultEquities(user) {
|
|
1489
|
+
return this.client.info.getUserVaultEquities(user);
|
|
1490
|
+
}
|
|
1491
|
+
async getUserRole(user) {
|
|
1492
|
+
return this.client.info.getUserRole(user);
|
|
1493
|
+
}
|
|
1494
|
+
async getDelegations(user) {
|
|
1495
|
+
return this.client.info.getDelegations(user);
|
|
1496
|
+
}
|
|
1497
|
+
async getDelegatorSummary(user) {
|
|
1498
|
+
return this.client.info.getDelegatorSummary(user);
|
|
1499
|
+
}
|
|
1500
|
+
async getDelegatorHistory(user) {
|
|
1501
|
+
return this.client.info.getDelegatorHistory(user);
|
|
1502
|
+
}
|
|
1503
|
+
async getDelegatorRewards(user) {
|
|
1504
|
+
return this.client.info.getDelegatorRewards(user);
|
|
1505
|
+
}
|
|
1506
|
+
async validatorSummaries() {
|
|
1507
|
+
return this.client.info.validatorSummaries();
|
|
1508
|
+
}
|
|
1509
|
+
async vaultSummaries() {
|
|
1510
|
+
return this.client.info.vaultSummaries();
|
|
1511
|
+
}
|
|
1512
|
+
async userFees(user) {
|
|
1513
|
+
return this.client.info.userFees(user);
|
|
1514
|
+
}
|
|
1515
|
+
async portfolio(user) {
|
|
1516
|
+
return this.client.info.portfolio(user);
|
|
1517
|
+
}
|
|
1518
|
+
async preTransferCheck(user, source) {
|
|
1519
|
+
return this.client.info.preTransferCheck(user, source);
|
|
1520
|
+
}
|
|
1521
|
+
async referral(user) {
|
|
1522
|
+
return this.client.info.referral(user);
|
|
1523
|
+
}
|
|
1524
|
+
async extraAgents(user) {
|
|
1525
|
+
return this.client.info.extraAgents(user);
|
|
1526
|
+
}
|
|
1527
|
+
async isVip(user) {
|
|
1528
|
+
return this.client.info.isVip(user);
|
|
1529
|
+
}
|
|
1530
|
+
async legalCheck(user) {
|
|
1531
|
+
return this.client.info.legalCheck(user);
|
|
1532
|
+
}
|
|
1533
|
+
async userTwapSliceFillsByTime(user, startTime, endTime) {
|
|
1534
|
+
return this.client.info.userTwapSliceFillsByTime(user, startTime, endTime);
|
|
1535
|
+
}
|
|
1536
|
+
async twapHistory(user) {
|
|
1537
|
+
return this.client.info.twapHistory(user);
|
|
1538
|
+
}
|
|
1539
|
+
async userToMultiSigSigners(user) {
|
|
1540
|
+
return this.client.info.userToMultiSigSigners(user);
|
|
1541
|
+
}
|
|
1542
|
+
async getBuilderFeeApproval(user, builderAddress) {
|
|
1543
|
+
return this.client.info.getBuilderFeeApproval(user, builderAddress);
|
|
1544
|
+
}
|
|
1545
|
+
async getUserOrderHistory(user, startTime, endTime) {
|
|
1546
|
+
return this.client.info.getUserOrderHistory(user, startTime, endTime);
|
|
1547
|
+
}
|
|
1548
|
+
// Account methods
|
|
1549
|
+
async getPosition(ticker) {
|
|
1550
|
+
if (!this.userAddress) {
|
|
1551
|
+
this.logger.warning(`Cannot get position: No user address provided`);
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
try {
|
|
1555
|
+
const state = await this.client.info.perpetuals.getClearinghouseState(this.userAddress);
|
|
1556
|
+
const positions = state.assetPositions;
|
|
1557
|
+
const normalizedTicker = ticker.toUpperCase();
|
|
1558
|
+
const position = positions.find((p) => p.position.coin === normalizedTicker);
|
|
1559
|
+
return position || null;
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
this.logger.error(`Error fetching position for ${ticker}`, error);
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async getPositions() {
|
|
1566
|
+
if (!this.userAddress) {
|
|
1567
|
+
this.logger.warning(`Cannot get positions: No user address provided`);
|
|
1568
|
+
return [];
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
const state = await this.client.info.perpetuals.getClearinghouseState(this.userAddress);
|
|
1572
|
+
return state.assetPositions;
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
this.logger.error(`Error fetching positions`, error);
|
|
1575
|
+
return [];
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// src/state/memory-state.ts
|
|
1581
|
+
var MemoryStateManager = class {
|
|
1582
|
+
storage = /* @__PURE__ */ new Map();
|
|
1583
|
+
async get(key) {
|
|
1584
|
+
return this.storage.get(key) ?? null;
|
|
1585
|
+
}
|
|
1586
|
+
async set(key, value) {
|
|
1587
|
+
this.storage.set(key, value);
|
|
1588
|
+
}
|
|
1589
|
+
async delete(key) {
|
|
1590
|
+
this.storage.delete(key);
|
|
1591
|
+
}
|
|
1592
|
+
async clear() {
|
|
1593
|
+
this.storage.clear();
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Get all keys (useful for debugging)
|
|
1597
|
+
*/
|
|
1598
|
+
getAllKeys() {
|
|
1599
|
+
return Array.from(this.storage.keys());
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
var RedisStateManager = class {
|
|
1603
|
+
client;
|
|
1604
|
+
strategyId;
|
|
1605
|
+
logger;
|
|
1606
|
+
constructor(redisUrl, strategyId, logger) {
|
|
1607
|
+
this.strategyId = strategyId;
|
|
1608
|
+
this.logger = logger;
|
|
1609
|
+
this.client = createClient({ url: redisUrl });
|
|
1610
|
+
this.client.on("error", (err) => {
|
|
1611
|
+
this.logger.error("Redis client error", err);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
async connect() {
|
|
1615
|
+
await this.client.connect();
|
|
1616
|
+
this.logger.info("Redis connected for state management");
|
|
1617
|
+
}
|
|
1618
|
+
async disconnect() {
|
|
1619
|
+
await this.client.quit();
|
|
1620
|
+
}
|
|
1621
|
+
getKey(key) {
|
|
1622
|
+
return `strategy:${this.strategyId}:state:${key}`;
|
|
1623
|
+
}
|
|
1624
|
+
async get(key) {
|
|
1625
|
+
const redisKey = this.getKey(key);
|
|
1626
|
+
const value = await this.client.get(redisKey);
|
|
1627
|
+
if (value === null) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
try {
|
|
1631
|
+
return superjson.parse(value);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
this.logger.error(`Failed to parse Redis value for key ${redisKey}`, error);
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async set(key, value) {
|
|
1638
|
+
const redisKey = this.getKey(key);
|
|
1639
|
+
const serialized = superjson.stringify(value);
|
|
1640
|
+
await this.client.set(redisKey, serialized);
|
|
1641
|
+
}
|
|
1642
|
+
async delete(key) {
|
|
1643
|
+
const redisKey = this.getKey(key);
|
|
1644
|
+
await this.client.del(redisKey);
|
|
1645
|
+
}
|
|
1646
|
+
async clear() {
|
|
1647
|
+
const pattern = `strategy:${this.strategyId}:state:*`;
|
|
1648
|
+
const keys = await this.client.keys(pattern);
|
|
1649
|
+
if (keys.length > 0) {
|
|
1650
|
+
await this.client.del(keys);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
// src/utils/logger.ts
|
|
1656
|
+
function createConsoleLogger() {
|
|
1657
|
+
return {
|
|
1658
|
+
info: (message, meta) => {
|
|
1659
|
+
console.log(`[INFO] ${message}`, meta ? JSON.stringify(meta, null, 2) : "");
|
|
1660
|
+
},
|
|
1661
|
+
error: (message, error) => {
|
|
1662
|
+
console.error(`[ERROR] ${message}`, error);
|
|
1663
|
+
},
|
|
1664
|
+
warning: (message, meta) => {
|
|
1665
|
+
console.warn(`[WARN] ${message}`, meta ? JSON.stringify(meta, null, 2) : "");
|
|
1666
|
+
},
|
|
1667
|
+
debug: (message, meta) => {
|
|
1668
|
+
console.debug(`[DEBUG] ${message}`, meta ? JSON.stringify(meta, null, 2) : "");
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function createOsirisContext(config) {
|
|
1673
|
+
const { strategyId, market, signer, polymarketOptions } = config;
|
|
1674
|
+
const logger = createConsoleLogger();
|
|
1675
|
+
const state = new MemoryStateManager();
|
|
1676
|
+
const marketLower = market.toLowerCase();
|
|
1677
|
+
const taService = new TechnicalAnalysisService({ logger });
|
|
1678
|
+
const context = {
|
|
1679
|
+
ta: createTAAPI(taService),
|
|
1680
|
+
state,
|
|
1681
|
+
log: createLogAPI(strategyId, logger),
|
|
1682
|
+
signer
|
|
1683
|
+
};
|
|
1684
|
+
const userAddress = config.userAddress;
|
|
1685
|
+
if (!userAddress) {
|
|
1686
|
+
throw new Error("User address not found");
|
|
1687
|
+
}
|
|
1688
|
+
if (marketLower === "polymarket") {
|
|
1689
|
+
context.polymarket = new PolymarketClient(logger, signer, userAddress, polymarketOptions);
|
|
1690
|
+
} else if (marketLower === "hyperliquid") {
|
|
1691
|
+
context.hyperliquid = new HyperliquidClient(logger, userAddress);
|
|
1692
|
+
} else {
|
|
1693
|
+
throw new Error(`Unsupported market: ${market}`);
|
|
1694
|
+
}
|
|
1695
|
+
return context;
|
|
1696
|
+
}
|
|
1697
|
+
function createTAAPI(taService) {
|
|
1698
|
+
return {
|
|
1699
|
+
calculate: (indicator, data, params) => {
|
|
1700
|
+
return taService.calculate(indicator, data, params || {});
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function createLogAPI(strategyId, logger) {
|
|
1705
|
+
return (message, meta) => {
|
|
1706
|
+
logger.info(`[Strategy ${strategyId}] ${message}`, meta);
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/engine/strategy-runner.ts
|
|
1711
|
+
async function runStrategy(strategy, config, context) {
|
|
1712
|
+
if (typeof strategy.shouldTrigger !== "function" || typeof strategy.onTrigger !== "function") {
|
|
1713
|
+
throw new Error("runStrategy requires a tick-based strategy with shouldTrigger and onTrigger methods");
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
const shouldTrigger = await strategy.shouldTrigger(context);
|
|
1717
|
+
if (shouldTrigger) {
|
|
1718
|
+
await strategy.onTrigger(context);
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
return false;
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
config.logger.error(`Error executing strategy: ${error.message}`, error);
|
|
1724
|
+
throw error;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
function createStrategyEngine(strategy, config, context) {
|
|
1728
|
+
return new StrategyEngine(strategy, config, context);
|
|
1729
|
+
}
|
|
1730
|
+
var StrategyEngine = class {
|
|
1731
|
+
constructor(strategy, config, context) {
|
|
1732
|
+
this.strategy = strategy;
|
|
1733
|
+
this.config = config;
|
|
1734
|
+
this.context = context;
|
|
1735
|
+
}
|
|
1736
|
+
intervalId = null;
|
|
1737
|
+
isRunning = false;
|
|
1738
|
+
/**
|
|
1739
|
+
* Start the engine with the configured tick interval
|
|
1740
|
+
*/
|
|
1741
|
+
start() {
|
|
1742
|
+
if (this.isRunning) {
|
|
1743
|
+
this.config.logger.warning("Strategy engine is already running");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const tickIntervalMs = this.config.tickIntervalMs || 6e4;
|
|
1747
|
+
this.isRunning = true;
|
|
1748
|
+
this.config.logger.info(`Starting strategy engine with ${tickIntervalMs}ms tick interval`);
|
|
1749
|
+
this.tick();
|
|
1750
|
+
this.intervalId = setInterval(() => {
|
|
1751
|
+
this.tick();
|
|
1752
|
+
}, tickIntervalMs);
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Stop the engine
|
|
1756
|
+
*/
|
|
1757
|
+
stop() {
|
|
1758
|
+
if (!this.isRunning) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
this.isRunning = false;
|
|
1762
|
+
if (this.intervalId) {
|
|
1763
|
+
clearInterval(this.intervalId);
|
|
1764
|
+
this.intervalId = null;
|
|
1765
|
+
}
|
|
1766
|
+
this.config.logger.info("Strategy engine stopped");
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Check if the engine is currently running
|
|
1770
|
+
*/
|
|
1771
|
+
isActive() {
|
|
1772
|
+
return this.isRunning;
|
|
1773
|
+
}
|
|
1774
|
+
async tick() {
|
|
1775
|
+
const tickStart = Date.now();
|
|
1776
|
+
this.config.logger.debug(`Tick start: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1777
|
+
try {
|
|
1778
|
+
await runStrategy(this.strategy, this.config, this.context);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
this.config.logger.error(`Error in strategy tick: ${error.message}`, error);
|
|
1781
|
+
}
|
|
1782
|
+
const tickDuration = Date.now() - tickStart;
|
|
1783
|
+
this.config.logger.debug(`Tick completed in ${tickDuration}ms`);
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
function isEventBasedStrategy(strategy) {
|
|
1787
|
+
return Array.isArray(strategy.subscriptions) && strategy.subscriptions.length > 0 && typeof strategy.onEvent === "function";
|
|
1788
|
+
}
|
|
1789
|
+
function isTickBasedStrategy(strategy) {
|
|
1790
|
+
return typeof strategy.shouldTrigger === "function" && typeof strategy.onTrigger === "function";
|
|
1791
|
+
}
|
|
1792
|
+
function validateStrategy(strategy) {
|
|
1793
|
+
const isEvent = isEventBasedStrategy(strategy);
|
|
1794
|
+
const isTick = isTickBasedStrategy(strategy);
|
|
1795
|
+
if (!isEvent && !isTick) {
|
|
1796
|
+
throw new Error(
|
|
1797
|
+
"Invalid strategy: must implement either tick-based methods (shouldTrigger + onTrigger) or event-based methods (subscriptions + onEvent)"
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
if (isEvent && isTick) {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
"Invalid strategy: strategies should not implement both tick-based and event-based methods. Choose one execution mode."
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
var EventRunner = class {
|
|
1807
|
+
constructor(strategy, config, context) {
|
|
1808
|
+
this.strategy = strategy;
|
|
1809
|
+
this.config = config;
|
|
1810
|
+
this.context = context;
|
|
1811
|
+
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
1812
|
+
throw new Error("Strategy must have subscriptions defined for event-based execution");
|
|
1813
|
+
}
|
|
1814
|
+
if (typeof this.strategy.onEvent !== "function") {
|
|
1815
|
+
throw new Error("Strategy must implement onEvent method for event-based execution");
|
|
1816
|
+
}
|
|
1817
|
+
this.maxReconnectAttempts = config.reconnect?.maxAttempts ?? 10;
|
|
1818
|
+
this.baseReconnectDelay = config.reconnect?.delayMs ?? 1e3;
|
|
1819
|
+
this.maxReconnectDelay = config.reconnect?.maxDelayMs ?? 3e4;
|
|
1820
|
+
}
|
|
1821
|
+
ws = null;
|
|
1822
|
+
isRunning = false;
|
|
1823
|
+
reconnectAttempts = 0;
|
|
1824
|
+
reconnectTimeout = null;
|
|
1825
|
+
isIntentionallyClosed = false;
|
|
1826
|
+
// Default reconnection settings
|
|
1827
|
+
maxReconnectAttempts;
|
|
1828
|
+
baseReconnectDelay;
|
|
1829
|
+
maxReconnectDelay;
|
|
1830
|
+
/**
|
|
1831
|
+
* Start listening to events
|
|
1832
|
+
* Connects to the WebSocket and subscribes to events
|
|
1833
|
+
*/
|
|
1834
|
+
start() {
|
|
1835
|
+
if (this.isRunning) {
|
|
1836
|
+
this.config.logger.warning("Event runner is already running");
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
this.isRunning = true;
|
|
1840
|
+
this.isIntentionallyClosed = false;
|
|
1841
|
+
this.reconnectAttempts = 0;
|
|
1842
|
+
const strategyName = this.config.strategyId || "unnamed-strategy";
|
|
1843
|
+
this.config.logger.info(`Starting event runner for strategy ${strategyName}`);
|
|
1844
|
+
this.config.logger.info(`Connecting to event source: ${this.config.eventSourceUrl}`);
|
|
1845
|
+
this.connect();
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Stop listening and disconnect from WebSocket
|
|
1849
|
+
*/
|
|
1850
|
+
stop() {
|
|
1851
|
+
if (!this.isRunning) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
this.isRunning = false;
|
|
1855
|
+
this.isIntentionallyClosed = true;
|
|
1856
|
+
if (this.reconnectTimeout) {
|
|
1857
|
+
clearTimeout(this.reconnectTimeout);
|
|
1858
|
+
this.reconnectTimeout = null;
|
|
1859
|
+
}
|
|
1860
|
+
if (this.ws) {
|
|
1861
|
+
try {
|
|
1862
|
+
this.unsubscribeFromEvents();
|
|
1863
|
+
this.ws.close(1e3, "Intentional close");
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
}
|
|
1866
|
+
this.ws = null;
|
|
1867
|
+
}
|
|
1868
|
+
this.config.logger.info("Event runner stopped");
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Check if the runner is currently active
|
|
1872
|
+
*/
|
|
1873
|
+
isActive() {
|
|
1874
|
+
return this.isRunning;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Get current connection status
|
|
1878
|
+
*/
|
|
1879
|
+
getStatus() {
|
|
1880
|
+
return {
|
|
1881
|
+
running: this.isRunning,
|
|
1882
|
+
connected: this.ws?.readyState === WebSocket.OPEN,
|
|
1883
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1884
|
+
subscriptionCount: this.strategy.subscriptions?.length ?? 0
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Connect to the WebSocket event source
|
|
1889
|
+
*/
|
|
1890
|
+
connect() {
|
|
1891
|
+
if (this.isIntentionallyClosed) {
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
try {
|
|
1895
|
+
this.ws = new WebSocket(this.config.eventSourceUrl);
|
|
1896
|
+
this.ws.on("open", () => {
|
|
1897
|
+
this.config.logger.info("Connected to event source");
|
|
1898
|
+
this.reconnectAttempts = 0;
|
|
1899
|
+
this.subscribeToEvents();
|
|
1900
|
+
});
|
|
1901
|
+
this.ws.on("message", (data) => {
|
|
1902
|
+
this.handleMessage(data);
|
|
1903
|
+
});
|
|
1904
|
+
this.ws.on("error", (error) => {
|
|
1905
|
+
this.config.logger.error(`WebSocket error: ${error.message}`, error);
|
|
1906
|
+
});
|
|
1907
|
+
this.ws.on("close", (code, reason) => {
|
|
1908
|
+
const reasonStr = reason.toString() || "none";
|
|
1909
|
+
this.config.logger.info(`WebSocket closed (code: ${code}, reason: ${reasonStr})`);
|
|
1910
|
+
if (!this.isIntentionallyClosed && this.isRunning) {
|
|
1911
|
+
this.attemptReconnect();
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
this.config.logger.error(`Failed to connect to WebSocket: ${error.message}`, error);
|
|
1916
|
+
if (this.isRunning && !this.isIntentionallyClosed) {
|
|
1917
|
+
this.attemptReconnect();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Attempt to reconnect with exponential backoff
|
|
1923
|
+
*/
|
|
1924
|
+
attemptReconnect() {
|
|
1925
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1926
|
+
this.config.logger.error(`Max reconnection attempts (${this.maxReconnectAttempts}) reached`);
|
|
1927
|
+
this.isRunning = false;
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
this.reconnectAttempts++;
|
|
1931
|
+
const delay = Math.min(
|
|
1932
|
+
this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1) + Math.random() * 1e3,
|
|
1933
|
+
this.maxReconnectDelay
|
|
1934
|
+
);
|
|
1935
|
+
this.config.logger.info(
|
|
1936
|
+
`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
1937
|
+
);
|
|
1938
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
1939
|
+
this.reconnectTimeout = null;
|
|
1940
|
+
this.connect();
|
|
1941
|
+
}, delay);
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Subscribe to events based on strategy's subscriptions
|
|
1945
|
+
*/
|
|
1946
|
+
subscribeToEvents() {
|
|
1947
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1948
|
+
this.config.logger.warning("Cannot subscribe - WebSocket not connected");
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
1952
|
+
this.config.logger.warning("No subscriptions defined for strategy");
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
const subscribeMessage = {
|
|
1956
|
+
action: "subscribe",
|
|
1957
|
+
subscriptions: this.strategy.subscriptions
|
|
1958
|
+
};
|
|
1959
|
+
try {
|
|
1960
|
+
this.ws.send(JSON.stringify(subscribeMessage));
|
|
1961
|
+
this.config.logger.info(
|
|
1962
|
+
`Subscribed to ${this.strategy.subscriptions.length} event(s): ` + this.strategy.subscriptions.map((s) => `${s.type}${s.market ? `@${s.market}` : ""}`).join(", ")
|
|
1963
|
+
);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
this.config.logger.error(`Failed to send subscription: ${error.message}`, error);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Unsubscribe from events before disconnecting
|
|
1970
|
+
*/
|
|
1971
|
+
unsubscribeFromEvents() {
|
|
1972
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
if (!this.strategy.subscriptions || this.strategy.subscriptions.length === 0) {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const unsubscribeMessage = {
|
|
1979
|
+
action: "unsubscribe",
|
|
1980
|
+
subscriptions: this.strategy.subscriptions
|
|
1981
|
+
};
|
|
1982
|
+
try {
|
|
1983
|
+
this.ws.send(JSON.stringify(unsubscribeMessage));
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Handle incoming WebSocket messages
|
|
1989
|
+
*/
|
|
1990
|
+
handleMessage(data) {
|
|
1991
|
+
try {
|
|
1992
|
+
const rawData = typeof data === "string" ? data : data.toString();
|
|
1993
|
+
if (rawData === "PING" || rawData === "PONG" || rawData.trim() === "") {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
let message;
|
|
1997
|
+
try {
|
|
1998
|
+
message = JSON.parse(rawData);
|
|
1999
|
+
} catch (parseError) {
|
|
2000
|
+
this.config.logger.warning(`Received non-JSON message: ${rawData.slice(0, 100)}`);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (!this.isValidEvent(message)) {
|
|
2004
|
+
this.config.logger.debug(`Received non-event message: ${JSON.stringify(message).slice(0, 200)}`);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
this.dispatchEvent(message);
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
this.config.logger.error(`Error handling message: ${error.message}`, error);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Validate that a message is a valid StrategyEvent
|
|
2014
|
+
*/
|
|
2015
|
+
isValidEvent(message) {
|
|
2016
|
+
return message && typeof message === "object" && typeof message.type === "string" && typeof message.timestamp === "number" && typeof message.data === "object";
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Dispatch an event to the strategy's onEvent handler
|
|
2020
|
+
*/
|
|
2021
|
+
async dispatchEvent(event) {
|
|
2022
|
+
const eventStart = Date.now();
|
|
2023
|
+
this.config.logger.debug(
|
|
2024
|
+
`Dispatching ${event.type} event${event.market ? ` for ${event.market}` : ""}`
|
|
2025
|
+
);
|
|
2026
|
+
try {
|
|
2027
|
+
if (this.strategy.onEvent) {
|
|
2028
|
+
await this.strategy.onEvent(event, this.context);
|
|
2029
|
+
}
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
this.config.logger.error(
|
|
2032
|
+
`Error in strategy onEvent handler: ${error.message}`,
|
|
2033
|
+
error
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
const eventDuration = Date.now() - eventStart;
|
|
2037
|
+
this.config.logger.debug(`Event handling completed in ${eventDuration}ms`);
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
function createEventRunner(strategy, config, context) {
|
|
2041
|
+
return new EventRunner(strategy, config, context);
|
|
2042
|
+
}
|
|
2043
|
+
var CHAIN_MAP = {
|
|
2044
|
+
"evm:eip155:1": mainnet,
|
|
2045
|
+
"evm:eip155:137": polygon,
|
|
2046
|
+
"evm:eip155:42161": arbitrum,
|
|
2047
|
+
"evm:eip155:10": optimism,
|
|
2048
|
+
"evm:eip155:8453": base,
|
|
2049
|
+
"evm:eip155:11155111": sepolia
|
|
2050
|
+
};
|
|
2051
|
+
var PrivateKeySigner = class {
|
|
2052
|
+
account;
|
|
2053
|
+
defaultChain;
|
|
2054
|
+
clientCache = /* @__PURE__ */ new Map();
|
|
2055
|
+
privateKey;
|
|
2056
|
+
constructor(config) {
|
|
2057
|
+
this.privateKey = config.privateKey.startsWith("0x") ? config.privateKey : `0x${config.privateKey}`;
|
|
2058
|
+
this.account = privateKeyToAccount(this.privateKey);
|
|
2059
|
+
this.defaultChain = config.chain || "evm:eip155:1";
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Get the wallet address derived from the private key
|
|
2063
|
+
*/
|
|
2064
|
+
getAddress() {
|
|
2065
|
+
return this.account.address;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Get the private key (needed for CLOB client initialization)
|
|
2069
|
+
* @returns The private key with 0x prefix
|
|
2070
|
+
*/
|
|
2071
|
+
getPrivateKey() {
|
|
2072
|
+
return this.privateKey;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Get or create a wallet client for the specified chain
|
|
2076
|
+
*/
|
|
2077
|
+
getClient(chain) {
|
|
2078
|
+
const chainId = chain || this.defaultChain;
|
|
2079
|
+
if (this.clientCache.has(chainId)) {
|
|
2080
|
+
return this.clientCache.get(chainId);
|
|
2081
|
+
}
|
|
2082
|
+
const viemChain = CHAIN_MAP[chainId];
|
|
2083
|
+
if (!viemChain) {
|
|
2084
|
+
throw new Error(`Unsupported chain: ${chainId}. Supported chains: ${Object.keys(CHAIN_MAP).join(", ")}`);
|
|
2085
|
+
}
|
|
2086
|
+
const client = createWalletClient({
|
|
2087
|
+
account: this.account,
|
|
2088
|
+
chain: viemChain,
|
|
2089
|
+
transport: http()
|
|
2090
|
+
});
|
|
2091
|
+
this.clientCache.set(chainId, client);
|
|
2092
|
+
return client;
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Sign a transaction with the private key
|
|
2096
|
+
* @param walletAddress - Ignored for private key signer (uses derived address)
|
|
2097
|
+
*/
|
|
2098
|
+
async signTransaction(transaction, chain, _walletAddress) {
|
|
2099
|
+
try {
|
|
2100
|
+
const client = this.getClient(chain);
|
|
2101
|
+
const txData = typeof transaction === "string" ? JSON.parse(transaction) : transaction;
|
|
2102
|
+
const signature = await client.signTransaction({
|
|
2103
|
+
account: this.account,
|
|
2104
|
+
...txData
|
|
2105
|
+
});
|
|
2106
|
+
return signature;
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
throw new Error(`PrivateKey signTransaction failed: ${error.message || error}`);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Sign typed data (EIP-712) with the private key
|
|
2113
|
+
* @param walletAddress - Ignored for private key signer (uses derived address)
|
|
2114
|
+
*/
|
|
2115
|
+
async signTypedData(typedData, chain, _walletAddress) {
|
|
2116
|
+
try {
|
|
2117
|
+
const client = this.getClient(chain);
|
|
2118
|
+
const signature = await client.signTypedData({
|
|
2119
|
+
account: this.account,
|
|
2120
|
+
domain: typedData.domain,
|
|
2121
|
+
types: typedData.types,
|
|
2122
|
+
primaryType: typedData.primaryType,
|
|
2123
|
+
message: typedData.message
|
|
2124
|
+
});
|
|
2125
|
+
return signature;
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
throw new Error(`PrivateKey signTypedData failed: ${error.message || error}`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Sign a message with the private key
|
|
2132
|
+
* @param walletAddress - Ignored for private key signer (uses derived address)
|
|
2133
|
+
*/
|
|
2134
|
+
async signMessage(message, chain, _walletAddress) {
|
|
2135
|
+
try {
|
|
2136
|
+
const client = this.getClient(chain);
|
|
2137
|
+
const messageToSign = message instanceof Uint8Array ? { raw: message } : message;
|
|
2138
|
+
const signature = await client.signMessage({
|
|
2139
|
+
account: this.account,
|
|
2140
|
+
message: messageToSign
|
|
2141
|
+
});
|
|
2142
|
+
return signature;
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
throw new Error(`PrivateKey signMessage failed: ${error.message || error}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
|
|
2149
|
+
// src/signer/index.ts
|
|
2150
|
+
var Signer = class {
|
|
2151
|
+
impl;
|
|
2152
|
+
constructor(config) {
|
|
2153
|
+
if (config.mode === "osiris") {
|
|
2154
|
+
this.impl = new OsirisSigner(config.config);
|
|
2155
|
+
} else if (config.mode === "privateKey") {
|
|
2156
|
+
this.impl = new PrivateKeySigner(config.config);
|
|
2157
|
+
} else {
|
|
2158
|
+
throw new Error(`Unsupported signer mode: ${config.mode}`);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Get the wallet address (only available for privateKey mode)
|
|
2163
|
+
*/
|
|
2164
|
+
getAddress() {
|
|
2165
|
+
if ("getAddress" in this.impl) {
|
|
2166
|
+
return this.impl.getAddress();
|
|
2167
|
+
}
|
|
2168
|
+
return void 0;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Sign a transaction via Osiris Hub
|
|
2172
|
+
*/
|
|
2173
|
+
async signTransaction(transaction, chain, walletAddress) {
|
|
2174
|
+
return this.impl.signTransaction(transaction, chain, walletAddress);
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Sign typed data (EIP-712 for EVM) via Osiris Hub
|
|
2178
|
+
*/
|
|
2179
|
+
async signTypedData(typedData, chain, walletAddress) {
|
|
2180
|
+
return this.impl.signTypedData(typedData, chain, walletAddress);
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Sign a message via Osiris Hub
|
|
2184
|
+
*/
|
|
2185
|
+
async signMessage(message, chain, walletAddress) {
|
|
2186
|
+
return this.impl.signMessage(message, chain, walletAddress);
|
|
2187
|
+
}
|
|
2188
|
+
getPrivateKey() {
|
|
2189
|
+
return this.impl.getPrivateKey();
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
|
|
2193
|
+
export { EventRunner, HyperliquidClient, MemoryStateManager, OsirisSigner, PolymarketClient, PrivateKeySigner, RedisStateManager, Signer, StrategyEngine, createConsoleLogger, createEventRunner, createOsirisContext, createStrategyEngine, isEventBasedStrategy, isTickBasedStrategy, runStrategy, validateStrategy };
|
|
2194
|
+
//# sourceMappingURL=index.js.map
|
|
2195
|
+
//# sourceMappingURL=index.js.map
|