@lasersell/lasersell-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 +21 -0
- package/README.md +114 -0
- package/dist/exitApi.d.ts +87 -0
- package/dist/exitApi.js +238 -0
- package/dist/exitApi.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/retry.d.ts +15 -0
- package/dist/retry.js +74 -0
- package/dist/retry.js.map +1 -0
- package/dist/stream/client.d.ts +144 -0
- package/dist/stream/client.js +995 -0
- package/dist/stream/client.js.map +1 -0
- package/dist/stream/index.d.ts +3 -0
- package/dist/stream/index.js +4 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/proto.d.ts +148 -0
- package/dist/stream/proto.js +266 -0
- package/dist/stream/proto.js.map +1 -0
- package/dist/stream/session.d.ts +64 -0
- package/dist/stream/session.js +243 -0
- package/dist/stream/session.js.map +1 -0
- package/dist/tx.d.ts +85 -0
- package/dist/tx.js +408 -0
- package/dist/tx.js.map +1 -0
- package/package.json +87 -0
- package/src/exitApi.ts +385 -0
- package/src/index.ts +5 -0
- package/src/retry.ts +102 -0
- package/src/stream/client.ts +1362 -0
- package/src/stream/index.ts +3 -0
- package/src/stream/proto.ts +565 -0
- package/src/stream/session.ts +372 -0
- package/src/tx.ts +617 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OptionalStrategyConfig,
|
|
3
|
+
type PositionSelectorInput,
|
|
4
|
+
StreamClient,
|
|
5
|
+
type StreamConfigure,
|
|
6
|
+
type StreamConnection,
|
|
7
|
+
type StreamSender,
|
|
8
|
+
strategyConfigFromOptional,
|
|
9
|
+
validateStrategyAndDeadline,
|
|
10
|
+
} from "./client.js";
|
|
11
|
+
import type { ServerMessage, StrategyConfigMsg } from "./proto.js";
|
|
12
|
+
|
|
13
|
+
export interface PositionHandle {
|
|
14
|
+
position_id: number;
|
|
15
|
+
token_account: string;
|
|
16
|
+
wallet_pubkey: string;
|
|
17
|
+
mint: string;
|
|
18
|
+
token_program?: string;
|
|
19
|
+
tokens: number;
|
|
20
|
+
entry_quote_units: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type StreamEvent =
|
|
24
|
+
| {
|
|
25
|
+
type: "message";
|
|
26
|
+
message: ServerMessage;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
type: "position_opened";
|
|
30
|
+
handle: PositionHandle;
|
|
31
|
+
message: ServerMessage;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: "position_closed";
|
|
35
|
+
handle?: PositionHandle;
|
|
36
|
+
message: ServerMessage;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
type: "exit_signal_with_tx";
|
|
40
|
+
handle?: PositionHandle;
|
|
41
|
+
message: ServerMessage;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: "pnl_update";
|
|
45
|
+
handle?: PositionHandle;
|
|
46
|
+
message: ServerMessage;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class StreamSession {
|
|
50
|
+
private readonly connection: StreamConnection;
|
|
51
|
+
private readonly positionsById = new Map<number, PositionHandle>();
|
|
52
|
+
private strategy: StrategyConfigMsg;
|
|
53
|
+
private deadlineTimeoutSec: number;
|
|
54
|
+
private readonly deadlineTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
55
|
+
private readonly openedAtMs = new Map<string, number>();
|
|
56
|
+
|
|
57
|
+
private constructor(
|
|
58
|
+
connection: StreamConnection,
|
|
59
|
+
strategy: StrategyConfigMsg,
|
|
60
|
+
deadlineTimeoutSec: number,
|
|
61
|
+
) {
|
|
62
|
+
this.connection = connection;
|
|
63
|
+
this.strategy = { ...strategy };
|
|
64
|
+
this.deadlineTimeoutSec = Math.max(0, deadlineTimeoutSec);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static async connect(
|
|
68
|
+
client: StreamClient,
|
|
69
|
+
configure: StreamConfigure,
|
|
70
|
+
): Promise<StreamSession> {
|
|
71
|
+
const connection = await client.connect(configure);
|
|
72
|
+
return StreamSession.fromConnection(
|
|
73
|
+
connection,
|
|
74
|
+
configure.strategy,
|
|
75
|
+
configure.deadline_timeout_sec ?? 0,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static fromConnection(
|
|
80
|
+
connection: StreamConnection,
|
|
81
|
+
strategy: StrategyConfigMsg = defaultStrategy(),
|
|
82
|
+
deadlineTimeoutSec = 0,
|
|
83
|
+
): StreamSession {
|
|
84
|
+
return new StreamSession(connection, strategy, deadlineTimeoutSec);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sender(): StreamSender {
|
|
88
|
+
return this.connection.sender();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
positions(): PositionHandle[] {
|
|
92
|
+
return [...this.positionsById.values()].map((position) => ({ ...position }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
positionsForWalletMint(wallet: string, mint: string): PositionHandle[] {
|
|
96
|
+
return this.positions()
|
|
97
|
+
.filter(
|
|
98
|
+
(position) =>
|
|
99
|
+
position.wallet_pubkey === wallet && position.mint === mint,
|
|
100
|
+
)
|
|
101
|
+
.map((position) => ({ ...position }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
close(): void;
|
|
105
|
+
close(handle: PositionHandle): void;
|
|
106
|
+
close(handle?: PositionHandle): void {
|
|
107
|
+
if (handle !== undefined) {
|
|
108
|
+
this.sender().closePosition(positionHandleToSelector(handle));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.cancelAllDeadlines();
|
|
112
|
+
this.connection.close();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
requestExitSignal(handle: PositionHandle, slippage_bps?: number): void {
|
|
116
|
+
this.sender().requestExitSignal(positionHandleToSelector(handle), slippage_bps);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
updateStrategy(
|
|
120
|
+
strategy: StrategyConfigMsg,
|
|
121
|
+
deadlineTimeoutSec = this.deadlineTimeoutSec,
|
|
122
|
+
): void {
|
|
123
|
+
validateStrategyAndDeadline(strategy, deadlineTimeoutSec);
|
|
124
|
+
this.strategy = { ...strategy };
|
|
125
|
+
this.deadlineTimeoutSec = Math.max(0, deadlineTimeoutSec);
|
|
126
|
+
this.rescheduleAllDeadlines();
|
|
127
|
+
this.sender().updateStrategy({ ...strategy });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
updateStrategyOptional(
|
|
131
|
+
strategy: OptionalStrategyConfig = {},
|
|
132
|
+
deadlineTimeoutSec = this.deadlineTimeoutSec,
|
|
133
|
+
): void {
|
|
134
|
+
this.updateStrategy(
|
|
135
|
+
strategyConfigFromOptional(strategy),
|
|
136
|
+
deadlineTimeoutSec,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async recv(): Promise<StreamEvent | null> {
|
|
141
|
+
const message = await this.connection.recv();
|
|
142
|
+
if (message === null) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return this.applyMessage(message);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private applyMessage(message: ServerMessage): StreamEvent {
|
|
150
|
+
switch (message.type) {
|
|
151
|
+
case "position_opened": {
|
|
152
|
+
const handle: PositionHandle = {
|
|
153
|
+
position_id: message.position_id,
|
|
154
|
+
token_account: message.token_account,
|
|
155
|
+
wallet_pubkey: message.wallet_pubkey,
|
|
156
|
+
mint: message.mint,
|
|
157
|
+
token_program: message.token_program,
|
|
158
|
+
tokens: message.tokens,
|
|
159
|
+
entry_quote_units: message.entry_quote_units,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
this.positionsById.set(message.position_id, handle);
|
|
163
|
+
this.armDeadline(handle.token_account);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
type: "position_opened",
|
|
167
|
+
handle: { ...handle },
|
|
168
|
+
message,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case "position_closed": {
|
|
172
|
+
const handle = this.removePosition(
|
|
173
|
+
message.position_id,
|
|
174
|
+
message.token_account,
|
|
175
|
+
);
|
|
176
|
+
const tokenAccount = handle?.token_account ?? message.token_account;
|
|
177
|
+
if (tokenAccount !== undefined) {
|
|
178
|
+
this.cancelDeadlineFor(tokenAccount);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
type: "position_closed",
|
|
183
|
+
handle: handle === undefined ? undefined : { ...handle },
|
|
184
|
+
message,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
case "exit_signal_with_tx": {
|
|
188
|
+
const handle = this.findPosition(message.position_id, message.token_account);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
type: "exit_signal_with_tx",
|
|
192
|
+
handle: handle === undefined ? undefined : { ...handle },
|
|
193
|
+
message,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
case "pnl_update": {
|
|
197
|
+
const handle = this.findPosition(message.position_id);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
type: "pnl_update",
|
|
201
|
+
handle: handle === undefined ? undefined : { ...handle },
|
|
202
|
+
message,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
default: {
|
|
206
|
+
return {
|
|
207
|
+
type: "message",
|
|
208
|
+
message,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private findPosition(
|
|
215
|
+
positionId: number,
|
|
216
|
+
tokenAccount?: string,
|
|
217
|
+
): PositionHandle | undefined {
|
|
218
|
+
const byId = this.positionsById.get(positionId);
|
|
219
|
+
if (byId !== undefined) {
|
|
220
|
+
return byId;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (tokenAccount === undefined) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const handle of this.positionsById.values()) {
|
|
228
|
+
if (handle.token_account === tokenAccount) {
|
|
229
|
+
return handle;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private removePosition(
|
|
237
|
+
positionId: number,
|
|
238
|
+
tokenAccount?: string,
|
|
239
|
+
): PositionHandle | undefined {
|
|
240
|
+
const byId = this.positionsById.get(positionId);
|
|
241
|
+
if (byId !== undefined) {
|
|
242
|
+
this.positionsById.delete(positionId);
|
|
243
|
+
return byId;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (tokenAccount === undefined) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const [id, handle] of this.positionsById.entries()) {
|
|
251
|
+
if (handle.token_account === tokenAccount) {
|
|
252
|
+
this.positionsById.delete(id);
|
|
253
|
+
return handle;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private deadlineMs(): number {
|
|
261
|
+
const raw = this.deadlineTimeoutSec;
|
|
262
|
+
const sec = Number.isFinite(raw) ? Math.max(0, raw) : 0;
|
|
263
|
+
return sec * 1_000;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private armDeadline(tokenAccount: string): void {
|
|
267
|
+
this.cancelDeadlineTimer(tokenAccount);
|
|
268
|
+
|
|
269
|
+
const deadlineMs = this.deadlineMs();
|
|
270
|
+
const openedAt = Date.now();
|
|
271
|
+
this.openedAtMs.set(tokenAccount, openedAt);
|
|
272
|
+
if (deadlineMs === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.scheduleDeadline(tokenAccount, deadlineMs);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private rescheduleAllDeadlines(): void {
|
|
280
|
+
this.cancelAllDeadlineTimers();
|
|
281
|
+
|
|
282
|
+
const deadlineMs = this.deadlineMs();
|
|
283
|
+
if (deadlineMs === 0) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const tokenAccounts = new Set(
|
|
289
|
+
[...this.positionsById.values()].map((position) => position.token_account),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
for (const tokenAccount of tokenAccounts) {
|
|
293
|
+
const openedAt = this.openedAtMs.get(tokenAccount) ?? now;
|
|
294
|
+
this.openedAtMs.set(tokenAccount, openedAt);
|
|
295
|
+
const remaining = openedAt + deadlineMs - now;
|
|
296
|
+
if (remaining <= 0) {
|
|
297
|
+
queueMicrotask(() => {
|
|
298
|
+
this.tryRequestExitSignal(tokenAccount);
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
this.scheduleDeadline(tokenAccount, remaining);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private scheduleDeadline(tokenAccount: string, delayMs: number): void {
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
this.deadlineTimers.delete(tokenAccount);
|
|
309
|
+
this.tryRequestExitSignal(tokenAccount);
|
|
310
|
+
}, Math.max(0, delayMs));
|
|
311
|
+
this.deadlineTimers.set(tokenAccount, timer);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private tryRequestExitSignal(tokenAccount: string): void {
|
|
315
|
+
if (!this.hasOpenPositionForTokenAccount(tokenAccount)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
this.sender().requestExitSignal(tokenAccount);
|
|
321
|
+
} catch {
|
|
322
|
+
// Ignore timer callback failures to avoid crashing caller loops.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private hasOpenPositionForTokenAccount(tokenAccount: string): boolean {
|
|
327
|
+
for (const handle of this.positionsById.values()) {
|
|
328
|
+
if (handle.token_account === tokenAccount) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private cancelDeadlineFor(tokenAccount: string): void {
|
|
336
|
+
this.cancelDeadlineTimer(tokenAccount);
|
|
337
|
+
this.openedAtMs.delete(tokenAccount);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private cancelDeadlineTimer(tokenAccount: string): void {
|
|
341
|
+
const timer = this.deadlineTimers.get(tokenAccount);
|
|
342
|
+
if (timer !== undefined) {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
this.deadlineTimers.delete(tokenAccount);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private cancelAllDeadlineTimers(): void {
|
|
349
|
+
for (const timer of this.deadlineTimers.values()) {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
}
|
|
352
|
+
this.deadlineTimers.clear();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private cancelAllDeadlines(): void {
|
|
356
|
+
this.cancelAllDeadlineTimers();
|
|
357
|
+
this.openedAtMs.clear();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function positionHandleToSelector(handle: PositionHandle): PositionSelectorInput {
|
|
362
|
+
return {
|
|
363
|
+
token_account: handle.token_account,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function defaultStrategy(): StrategyConfigMsg {
|
|
368
|
+
return {
|
|
369
|
+
target_profit_pct: 0,
|
|
370
|
+
stop_loss_pct: 0,
|
|
371
|
+
};
|
|
372
|
+
}
|