@solard/sdk 0.2.31 → 0.2.37

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/src/index.ts CHANGED
@@ -1,38 +1,41 @@
1
1
  export { default } from "./default.ts";
2
2
  export { createSolard } from "./client.ts";
3
- export {
4
- normalizeValueBandPolicy,
5
- planValueBandDecision,
6
- valueBandThresholds,
7
- normalizeTargetWeightPolicy,
8
- planTargetWeightRebalance,
9
- buildHistoricalCandles,
10
- runHistoricalStrategy,
11
- sweepHistoricalStrategies,
12
- LAUNCHLAB_PLATFORM_PRESETS,
13
- resolveLaunchLabPlatform,
14
- } from "@solard/core";
15
- export type {
16
- ValueBandPolicy,
17
- ValueBandRuntimeState,
18
- ValueBandDecision,
19
- TargetWeightPolicy,
20
- TargetWeightRebalancePlan,
21
- HistoricalTradeTape,
22
- HistoricalCandle,
23
- HistoricalStrategy,
24
- HistoricalExecutionOptions,
25
- HistoricalStrategyResult,
26
- CreatorFeeDiscovery,
27
- CreatorFeeGroup,
28
- CreatorFeeItem,
29
- WalletLedger,
30
- WalletLedgerEntry,
31
- WalletLedgerOptions,
32
- CurrentMarket,
33
- PreparedTokenDeployment,
34
- PrepareDeploymentArgs,
35
- } from "@solard/core";
3
+ export { createTraderSolard } from "@solard/core";
4
+ export {
5
+ normalizeValueBandPolicy,
6
+ planValueBandDecision,
7
+ valueBandThresholds,
8
+ normalizeTargetWeightPolicy,
9
+ planTargetWeightRebalance,
10
+ buildHistoricalCandles,
11
+ runHistoricalStrategy,
12
+ sweepHistoricalStrategies,
13
+ LAUNCHLAB_PLATFORM_PRESETS,
14
+ resolveLaunchLabPlatform,
15
+ } from "@solard/core";
16
+ export type {
17
+ SellEconomics,
18
+ SupportedLaunchLabPair,
19
+ ValueBandPolicy,
20
+ ValueBandRuntimeState,
21
+ ValueBandDecision,
22
+ TargetWeightPolicy,
23
+ TargetWeightRebalancePlan,
24
+ HistoricalTradeTape,
25
+ HistoricalCandle,
26
+ HistoricalStrategy,
27
+ HistoricalExecutionOptions,
28
+ HistoricalStrategyResult,
29
+ CreatorFeeDiscovery,
30
+ CreatorFeeGroup,
31
+ CreatorFeeItem,
32
+ WalletLedger,
33
+ WalletLedgerEntry,
34
+ WalletLedgerOptions,
35
+ CurrentMarket,
36
+ PreparedTokenDeployment,
37
+ PrepareDeploymentArgs,
38
+ } from "@solard/core";
36
39
  export type {
37
40
  Solard,
38
41
  SolardBuyInput,
@@ -53,8 +56,8 @@ export {
53
56
 
54
57
  export type {
55
58
  ClaimCreatorRewardsOptions,
56
- TradeLandingPolicy,
57
- LivePoolReserves,
59
+ TradeLandingPolicy,
60
+ LivePoolReserves,
58
61
  CreatorRewardClaimPayout,
59
62
  CreatorRewardClaimResult,
60
63
  CumulativeDistributionExecuteOptions,
@@ -117,13 +120,13 @@ export {
117
120
  subscribeLaunches,
118
121
  subscribeMigrations,
119
122
  } from "./live.ts";
120
- export type {
121
- ListenTradesOptions,
122
- LiveEndpointOptions,
123
+ export type {
124
+ ListenTradesOptions,
125
+ LiveEndpointOptions,
123
126
  SubscribeLaunchesOptions,
124
127
  SubscribeMigrationsOptions,
125
- TradeListener,
126
- TradeListenerMigration,
128
+ TradeListener,
129
+ TradeListenerMigration,
127
130
  } from "./live.ts";
128
131
  export type {
129
132
  LaunchEvent,
package/src/live.test.ts CHANGED
@@ -1,6 +1,16 @@
1
- import { describe, expect, spyOn, test } from "bun:test";
2
- import { Connection } from "@solana/web3.js";
3
- import { deriveWebSocketEndpoint, LiveEndpointPool, redactRpcEndpoint, subscribeTrades } from "./live.ts";
1
+ import { describe, expect, spyOn, test } from "bun:test";
2
+ import { Connection } from "@solana/web3.js";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { openDatabase, closeDatabase } from "../../core/src/db/database.ts";
7
+ import { SqliteTokenHistoryRepository } from "../../core/src/chain/token-history/repository.ts";
8
+ import {
9
+ deriveWebSocketEndpoint,
10
+ LiveEndpointPool,
11
+ redactRpcEndpoint,
12
+ subscribeTrades,
13
+ } from "./live.ts";
4
14
 
5
15
  describe("sdk live endpoint", () => {
6
16
  test("derives websocket from the one RPC endpoint", () => {
@@ -18,96 +28,233 @@ describe("sdk live endpoint", () => {
18
28
  ),
19
29
  ).toBe("https://mainnet.helius-rpc.com/?api-key=<redacted>&foo=bar");
20
30
  });
21
- });
22
- test("live endpoint selection requires readiness, sticks, and rotates after later failures", async () => {
23
- const calls: string[] = [];
24
- const unavailable = new Set(["https://first.example/"]);
25
- const pool = new LiveEndpointPool(["https://first.example/", "https://second.example/", "https://public.example/"], async (endpoint) => {
26
- calls.push(endpoint);
27
- if (unavailable.has(endpoint)) throw new Error("probe failed");
28
- });
29
- expect(await pool.select()).toBe("https://second.example/");
30
- calls.length = 0;
31
- expect(await pool.select()).toBe("https://second.example/");
32
- expect(calls).toEqual(["https://second.example/"]);
33
- unavailable.add("https://second.example/");
34
- expect(await pool.check()).toBe("https://public.example/");
35
- unavailable.add("https://public.example/");
36
- await expect(pool.check()).rejects.toThrow("probe failed");
37
- });
38
-
39
- test("pooled trade stream preserves dynamic watched tokens when it rotates and closes", async () => {
40
- const originalSocket = globalThis.WebSocket;
41
- let firstHealthy = true;
42
- let nextId = 0;
43
- const registrations: Array<{ endpoint: string; token: string }> = [];
44
- const first = "https://live-primary.example/";
45
- const second = "https://live-fallback.example/";
46
- const mint = "So11111111111111111111111111111111111111112";
47
- globalThis.WebSocket = class extends EventTarget {
48
- constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); }
49
- close() {}
50
- } as any;
51
- const fetchMock = spyOn(globalThis, "fetch").mockImplementation(async (input) => {
52
- if (String(input) === first && !firstHealthy) return new Response("", { status: 401 });
53
- return new Response(JSON.stringify({ result: "ok" }));
54
- });
55
- const logsMock = spyOn(Connection.prototype, "onLogs").mockImplementation(function (token) {
56
- registrations.push({ endpoint: this.rpcEndpoint, token: String(token) });
57
- return ++nextId;
58
- });
59
- const removeMock = spyOn(Connection.prototype, "removeOnLogsListener").mockResolvedValue(undefined);
60
- let subscription: Awaited<ReturnType<typeof subscribeTrades>> | undefined;
61
- try {
62
- subscription = await subscribeTrades({ tokens: [], rpcUrls: [first, second], healthCheckIntervalMs: 1000, onTrade() {} });
63
- await subscription.addTokens(mint);
64
- firstHealthy = false;
65
- const deadline = Date.now() + 3000;
66
- while (!registrations.some((row) => row.endpoint === second) && Date.now() < deadline) await Bun.sleep(25);
67
- expect(registrations).toEqual([{ endpoint: first, token: mint }, { endpoint: second, token: mint }]);
68
- expect(subscription.listTokens()).toEqual([mint]);
69
- expect(subscription.hasToken(mint)).toBe(true);
70
- await subscription.close();
71
- await subscription.closed;
72
- expect(subscription.listTokens()).toEqual([]);
73
- expect(removeMock).toHaveBeenCalledTimes(2);
74
- } finally {
75
- await subscription?.close();
76
- logsMock.mockRestore(); removeMock.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket;
77
- }
78
- });
79
-
80
- test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
81
- const core = await import("@solard/core");
82
- const { listenTrades } = await import("./live.ts");
83
- const mint = "So11111111111111111111111111111111111111112";
84
- const endpoint = "https://migration-listener.example/";
85
- const originalSocket = globalThis.WebSocket;
86
- globalThis.WebSocket = class extends EventTarget { constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); } close() {} } as any;
87
- const fetchMock = spyOn(globalThis, "fetch").mockImplementation(async () => new Response(JSON.stringify({ result: "ok" })));
88
- let onMigration: any; let onTrade: any;
89
- const tokens = new Set<string>(); const changes: string[] = [];
90
- let closeTrade!: () => void;
91
- const tradeClosed = new Promise<void>(resolve => { closeTrade = resolve; });
92
- const tradeMock = spyOn(core, "subscribeTrades").mockImplementation(async (options: any) => {
93
- options.tokens.forEach((t: string) => tokens.add(t)); onTrade = options.onTrade;
94
- return { addTokens: async (value: any) => { changes.push("add"); (typeof value === "string" ? [value] : value).forEach((t: string) => tokens.add(t)); },
95
- removeTokens: async (value: any) => { changes.push("remove"); (typeof value === "string" ? [value] : value).forEach((t: string) => tokens.delete(t)); },
96
- hasToken: (t: string) => tokens.has(t), listTokens: () => [...tokens], close: async () => closeTrade(), closed: tradeClosed };
97
- });
98
- const migrationMock = spyOn(core, "subscribeMigrations").mockImplementation(async (options: any) => {
99
- onMigration = options.onMigration;
100
- return { mode: "selected", addTokens: async () => {}, removeTokens: async () => {}, hasToken: () => true, listTokens: () => options.tokens, close: async () => {}, closed: Promise.resolve() } as any;
101
- });
102
- let listener: Awaited<ReturnType<typeof listenTrades>> | undefined;
103
- try {
104
- listener = await listenTrades({ tokens: [mint], rpcUrls: [endpoint] });
105
- const received: any[] = []; listener.onMigration(event => { received.push(event); });
106
- await onTrade({ signature: "trade", mint, pool: "old-pool" });
107
- await onMigration({ type: "migration", mint, pool: "new-pool", signature: "migration", slot: 12, venue: "pump", destination: "pumpswap", quoteMint: mint, atMs: Date.now(), isMayhemMode: false, metadata: null });
108
- expect(changes).toEqual(["remove", "add"]);
109
- expect(listener.list()).toEqual([mint]);
110
- expect(received[0]).toMatchObject({ oldPool: "old-pool", newPool: "new-pool", slot: 12 });
111
- await listener.close(); await listener.closed;
112
- } finally { await listener?.close(); tradeMock.mockRestore(); migrationMock.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket; }
113
- });
31
+ });
32
+ test("live endpoint selection requires readiness, sticks, and rotates after later failures", async () => {
33
+ const calls: string[] = [];
34
+ const unavailable = new Set(["https://first.example/"]);
35
+ const pool = new LiveEndpointPool(
36
+ [
37
+ "https://first.example/",
38
+ "https://second.example/",
39
+ "https://public.example/",
40
+ ],
41
+ async (endpoint) => {
42
+ calls.push(endpoint);
43
+ if (unavailable.has(endpoint)) throw new Error("probe failed");
44
+ },
45
+ );
46
+ expect(await pool.select()).toBe("https://second.example/");
47
+ calls.length = 0;
48
+ expect(await pool.select()).toBe("https://second.example/");
49
+ expect(calls).toEqual(["https://second.example/"]);
50
+ unavailable.add("https://second.example/");
51
+ expect(await pool.check()).toBe("https://public.example/");
52
+ unavailable.add("https://public.example/");
53
+ await expect(pool.check()).rejects.toThrow("probe failed");
54
+ });
55
+
56
+ test("pooled trade stream preserves dynamic watched tokens when it rotates and closes", async () => {
57
+ const originalSocket = globalThis.WebSocket;
58
+ let firstHealthy = true;
59
+ let nextId = 0;
60
+ const registrations: Array<{ endpoint: string; token: string }> = [];
61
+ const first = "https://live-primary.example/";
62
+ const second = "https://live-fallback.example/";
63
+ const mint = "So11111111111111111111111111111111111111112";
64
+ globalThis.WebSocket = class extends EventTarget {
65
+ constructor() {
66
+ super();
67
+ queueMicrotask(() => this.dispatchEvent(new Event("open")));
68
+ }
69
+ close() {}
70
+ } as any;
71
+ const fetchMock = spyOn(globalThis, "fetch").mockImplementation(
72
+ async (input) => {
73
+ if (String(input) === first && !firstHealthy)
74
+ return new Response("", { status: 401 });
75
+ return new Response(JSON.stringify({ result: "ok" }));
76
+ },
77
+ );
78
+ const logsMock = spyOn(Connection.prototype, "onLogs").mockImplementation(
79
+ function (token) {
80
+ registrations.push({ endpoint: this.rpcEndpoint, token: String(token) });
81
+ return ++nextId;
82
+ },
83
+ );
84
+ const removeMock = spyOn(
85
+ Connection.prototype,
86
+ "removeOnLogsListener",
87
+ ).mockResolvedValue(undefined);
88
+ let subscription: Awaited<ReturnType<typeof subscribeTrades>> | undefined;
89
+ try {
90
+ subscription = await subscribeTrades({
91
+ tokens: [],
92
+ rpcUrls: [first, second],
93
+ healthCheckIntervalMs: 1000,
94
+ onTrade() {},
95
+ });
96
+ await subscription.addTokens(mint);
97
+ firstHealthy = false;
98
+ const deadline = Date.now() + 3000;
99
+ while (
100
+ !registrations.some((row) => row.endpoint === second) &&
101
+ Date.now() < deadline
102
+ )
103
+ await Bun.sleep(25);
104
+ expect(registrations).toEqual([
105
+ { endpoint: first, token: mint },
106
+ { endpoint: second, token: mint },
107
+ ]);
108
+ expect(subscription.listTokens()).toEqual([mint]);
109
+ expect(subscription.hasToken(mint)).toBe(true);
110
+ await subscription.close();
111
+ await subscription.closed;
112
+ expect(subscription.listTokens()).toEqual([]);
113
+ expect(removeMock).toHaveBeenCalledTimes(2);
114
+ } finally {
115
+ await subscription?.close();
116
+ logsMock.mockRestore();
117
+ removeMock.mockRestore();
118
+ fetchMock.mockRestore();
119
+ globalThis.WebSocket = originalSocket;
120
+ }
121
+ });
122
+
123
+ test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
124
+ const core = await import("@solard/core");
125
+ const { listenTrades } = await import("./live.ts");
126
+ const mint = "So11111111111111111111111111111111111111112";
127
+ const endpoint = "https://migration-listener.example/";
128
+ const dir = mkdtempSync(join(tmpdir(), "slrd-sdk-listener-"));
129
+ const dbPath = join(dir, "history.sqlite");
130
+ const originalSocket = globalThis.WebSocket;
131
+ globalThis.WebSocket = class extends EventTarget {
132
+ constructor() {
133
+ super();
134
+ queueMicrotask(() => this.dispatchEvent(new Event("open")));
135
+ }
136
+ close() {}
137
+ } as any;
138
+ const fetchMock = spyOn(globalThis, "fetch").mockImplementation(
139
+ async () => new Response(JSON.stringify({ result: "ok" })),
140
+ );
141
+ let onMigration: any;
142
+ let onTrade: any;
143
+ const tokens = new Set<string>();
144
+ const changes: string[] = [];
145
+ let closeTrade!: () => void;
146
+ const tradeClosed = new Promise<void>((resolve) => {
147
+ closeTrade = resolve;
148
+ });
149
+ const tradeMock = spyOn(core, "subscribeTrades").mockImplementation(
150
+ async (options: any) => {
151
+ options.tokens.forEach((t: string) => tokens.add(t));
152
+ onTrade = options.onTrade;
153
+ return {
154
+ addTokens: async (value: any) => {
155
+ changes.push("add");
156
+ (typeof value === "string" ? [value] : value).forEach((t: string) =>
157
+ tokens.add(t),
158
+ );
159
+ },
160
+ removeTokens: async (value: any) => {
161
+ changes.push("remove");
162
+ (typeof value === "string" ? [value] : value).forEach((t: string) =>
163
+ tokens.delete(t),
164
+ );
165
+ },
166
+ hasToken: (t: string) => tokens.has(t),
167
+ listTokens: () => [...tokens],
168
+ close: async () => closeTrade(),
169
+ closed: tradeClosed,
170
+ };
171
+ },
172
+ );
173
+ const migrationMock = spyOn(core, "subscribeMigrations").mockImplementation(
174
+ async (options: any) => {
175
+ onMigration = options.onMigration;
176
+ return {
177
+ mode: "selected",
178
+ addTokens: async () => {},
179
+ removeTokens: async () => {},
180
+ hasToken: () => true,
181
+ listTokens: () => options.tokens,
182
+ close: async () => {},
183
+ closed: Promise.resolve(),
184
+ } as any;
185
+ },
186
+ );
187
+ let listener: Awaited<ReturnType<typeof listenTrades>> | undefined;
188
+ try {
189
+ listener = await listenTrades({
190
+ tokens: [mint],
191
+ rpcUrls: [endpoint],
192
+ dbPath,
193
+ });
194
+ const received: any[] = [];
195
+ listener.onMigration((event) => {
196
+ received.push(event);
197
+ });
198
+ const event = {
199
+ signature: "trade",
200
+ mint,
201
+ pool: "old-pool",
202
+ venue: "pumpswap",
203
+ side: "buy",
204
+ eventIndex: 0,
205
+ slot: 11,
206
+ atMs: Date.now(),
207
+ baseRaw: 1000000n,
208
+ quoteRaw: 10000000n,
209
+ market: {
210
+ quoteMint: mint,
211
+ baseDecimals: 6,
212
+ quoteDecimals: 9,
213
+ supplyRaw: 1000000000000000n,
214
+ baseReserveRaw: 100000000n,
215
+ quoteReserveRaw: 1000000000n,
216
+ priceQuotePerToken: 0.01,
217
+ priceSol: 0.01,
218
+ priceUsd: 1,
219
+ marketCapSol: 10000000,
220
+ marketCapUsd: 1000000000,
221
+ },
222
+ };
223
+ await onTrade(event);
224
+ await onTrade(event);
225
+ await onMigration({
226
+ type: "migration",
227
+ mint,
228
+ pool: "new-pool",
229
+ signature: "migration",
230
+ slot: 12,
231
+ venue: "pump",
232
+ destination: "pumpswap",
233
+ quoteMint: mint,
234
+ atMs: Date.now(),
235
+ isMayhemMode: false,
236
+ metadata: null,
237
+ });
238
+ expect(changes).toEqual(["remove", "add"]);
239
+ expect(listener.list()).toEqual([mint]);
240
+ expect(received[0]).toMatchObject({
241
+ oldPool: "old-pool",
242
+ newPool: "new-pool",
243
+ slot: 12,
244
+ });
245
+ await listener.close();
246
+ await listener.closed;
247
+ const repository = new SqliteTokenHistoryRepository(openDatabase(dbPath));
248
+ expect(repository.loadTrades(mint)).toHaveLength(1);
249
+ expect(repository.loadCandles1s(mint)).toHaveLength(1);
250
+ closeDatabase(dbPath);
251
+ } finally {
252
+ await listener?.close();
253
+ closeDatabase(dbPath);
254
+ tradeMock.mockRestore();
255
+ migrationMock.mockRestore();
256
+ fetchMock.mockRestore();
257
+ globalThis.WebSocket = originalSocket;
258
+ rmSync(dir, { recursive: true, force: true });
259
+ }
260
+ });