@solard/sdk 0.2.33 → 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.
@@ -30,11 +30,18 @@ describe("sdk membrane", () => {
30
30
  test("has an exact curated root export surface", () => {
31
31
  expect(exportedNames(source)).toEqual({
32
32
  values: [
33
- "normalizeValueBandPolicy", "planValueBandDecision", "valueBandThresholds",
34
- "normalizeTargetWeightPolicy", "planTargetWeightRebalance",
35
- "buildHistoricalCandles", "runHistoricalStrategy", "sweepHistoricalStrategies",
36
- "LAUNCHLAB_PLATFORM_PRESETS", "resolveLaunchLabPlatform",
37
- "createSolard",
33
+ "normalizeValueBandPolicy",
34
+ "planValueBandDecision",
35
+ "valueBandThresholds",
36
+ "normalizeTargetWeightPolicy",
37
+ "planTargetWeightRebalance",
38
+ "buildHistoricalCandles",
39
+ "runHistoricalStrategy",
40
+ "sweepHistoricalStrategies",
41
+ "LAUNCHLAB_PLATFORM_PRESETS",
42
+ "resolveLaunchLabPlatform",
43
+ "createSolard",
44
+ "createTraderSolard",
38
45
  "default",
39
46
  "fetchTokenMetadata",
40
47
  "formatRaw",
@@ -47,17 +54,29 @@ describe("sdk membrane", () => {
47
54
  "sol",
48
55
  "tokenAmount",
49
56
  ].sort(),
50
- types: [
51
- "SellEconomics",
52
- "SupportedLaunchLabPair",
57
+ types: [
58
+ "SellEconomics",
59
+ "SupportedLaunchLabPair",
53
60
  "TradeListenerMigration",
54
- "ValueBandPolicy", "ValueBandRuntimeState", "ValueBandDecision",
55
- "TargetWeightPolicy", "TargetWeightRebalancePlan",
56
- "HistoricalTradeTape", "HistoricalCandle", "HistoricalStrategy",
57
- "HistoricalExecutionOptions", "HistoricalStrategyResult",
58
- "CreatorFeeDiscovery", "CreatorFeeGroup", "CreatorFeeItem",
59
- "WalletLedger", "WalletLedgerEntry", "WalletLedgerOptions", "CurrentMarket",
60
- "PreparedTokenDeployment", "PrepareDeploymentArgs",
61
+ "ValueBandPolicy",
62
+ "ValueBandRuntimeState",
63
+ "ValueBandDecision",
64
+ "TargetWeightPolicy",
65
+ "TargetWeightRebalancePlan",
66
+ "HistoricalTradeTape",
67
+ "HistoricalCandle",
68
+ "HistoricalStrategy",
69
+ "HistoricalExecutionOptions",
70
+ "HistoricalStrategyResult",
71
+ "CreatorFeeDiscovery",
72
+ "CreatorFeeGroup",
73
+ "CreatorFeeItem",
74
+ "WalletLedger",
75
+ "WalletLedgerEntry",
76
+ "WalletLedgerOptions",
77
+ "CurrentMarket",
78
+ "PreparedTokenDeployment",
79
+ "PrepareDeploymentArgs",
61
80
  "TradeLandingPolicy",
62
81
  "LiveEndpointOptions",
63
82
  "LivePoolReserves",
@@ -177,7 +196,7 @@ describe("sdk membrane", () => {
177
196
  expect(defaultClient).toContain("instance ??= createSolard()");
178
197
  expect(defaultClient).toContain('if (key === "close")');
179
198
  expect(defaultClient).toContain("getSolUsdPrice");
180
- expect(client).toContain("listenTrades:");
199
+ expect(client).toContain("listenTrades:");
181
200
  expect(defaultClient).toContain("subscribeLaunches");
182
201
  expect(defaultClient).toContain("subscribeMigrations");
183
202
  expect(defaultClient).not.toContain("export default createSolard()");
@@ -202,7 +221,7 @@ describe("sdk membrane", () => {
202
221
 
203
222
  test("does not expose core repositories or secret-bearing handles", () => {
204
223
  expect(client).not.toMatch(
205
- /^(?: {2}| {4})(?:db|wallets|tokens|groups|executions|positions|prices|alts|senders|venues|claimSources|launches|launchpads|connection|signer)\s*:/m,
224
+ /^(?: {2}| {4})(?:db|wallets|tokens|groups|executions|positions|prices|alts|senders|venues|claimSources|launches|launchpads|connection|signer)\s*:/m,
206
225
  );
207
226
  });
208
227
  });
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default } from "./default.ts";
2
- export { createSolard } from "./client.ts";
2
+ export { createSolard } from "./client.ts";
3
+ export { createTraderSolard } from "@solard/core";
3
4
  export {
4
5
  normalizeValueBandPolicy,
5
6
  planValueBandDecision,
@@ -12,9 +13,9 @@ export {
12
13
  LAUNCHLAB_PLATFORM_PRESETS,
13
14
  resolveLaunchLabPlatform,
14
15
  } from "@solard/core";
15
- export type {
16
- SellEconomics,
17
- SupportedLaunchLabPair,
16
+ export type {
17
+ SellEconomics,
18
+ SupportedLaunchLabPair,
18
19
  ValueBandPolicy,
19
20
  ValueBandRuntimeState,
20
21
  ValueBandDecision,
package/src/live.test.ts CHANGED
@@ -1,11 +1,16 @@
1
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 { deriveWebSocketEndpoint, LiveEndpointPool, redactRpcEndpoint, subscribeTrades } from "./live.ts";
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";
9
14
 
10
15
  describe("sdk live endpoint", () => {
11
16
  test("derives websocket from the one RPC endpoint", () => {
@@ -27,10 +32,17 @@ describe("sdk live endpoint", () => {
27
32
  test("live endpoint selection requires readiness, sticks, and rotates after later failures", async () => {
28
33
  const calls: string[] = [];
29
34
  const unavailable = new Set(["https://first.example/"]);
30
- const pool = new LiveEndpointPool(["https://first.example/", "https://second.example/", "https://public.example/"], async (endpoint) => {
31
- calls.push(endpoint);
32
- if (unavailable.has(endpoint)) throw new Error("probe failed");
33
- });
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
+ );
34
46
  expect(await pool.select()).toBe("https://second.example/");
35
47
  calls.length = 0;
36
48
  expect(await pool.select()).toBe("https://second.example/");
@@ -50,26 +62,49 @@ test("pooled trade stream preserves dynamic watched tokens when it rotates and c
50
62
  const second = "https://live-fallback.example/";
51
63
  const mint = "So11111111111111111111111111111111111111112";
52
64
  globalThis.WebSocket = class extends EventTarget {
53
- constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); }
65
+ constructor() {
66
+ super();
67
+ queueMicrotask(() => this.dispatchEvent(new Event("open")));
68
+ }
54
69
  close() {}
55
70
  } as any;
56
- const fetchMock = spyOn(globalThis, "fetch").mockImplementation(async (input) => {
57
- if (String(input) === first && !firstHealthy) return new Response("", { status: 401 });
58
- return new Response(JSON.stringify({ result: "ok" }));
59
- });
60
- const logsMock = spyOn(Connection.prototype, "onLogs").mockImplementation(function (token) {
61
- registrations.push({ endpoint: this.rpcEndpoint, token: String(token) });
62
- return ++nextId;
63
- });
64
- const removeMock = spyOn(Connection.prototype, "removeOnLogsListener").mockResolvedValue(undefined);
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);
65
88
  let subscription: Awaited<ReturnType<typeof subscribeTrades>> | undefined;
66
89
  try {
67
- subscription = await subscribeTrades({ tokens: [], rpcUrls: [first, second], healthCheckIntervalMs: 1000, onTrade() {} });
90
+ subscription = await subscribeTrades({
91
+ tokens: [],
92
+ rpcUrls: [first, second],
93
+ healthCheckIntervalMs: 1000,
94
+ onTrade() {},
95
+ });
68
96
  await subscription.addTokens(mint);
69
97
  firstHealthy = false;
70
98
  const deadline = Date.now() + 3000;
71
- while (!registrations.some((row) => row.endpoint === second) && Date.now() < deadline) await Bun.sleep(25);
72
- expect(registrations).toEqual([{ endpoint: first, token: mint }, { endpoint: second, token: mint }]);
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
+ ]);
73
108
  expect(subscription.listTokens()).toEqual([mint]);
74
109
  expect(subscription.hasToken(mint)).toBe(true);
75
110
  await subscription.close();
@@ -78,50 +113,148 @@ test("pooled trade stream preserves dynamic watched tokens when it rotates and c
78
113
  expect(removeMock).toHaveBeenCalledTimes(2);
79
114
  } finally {
80
115
  await subscription?.close();
81
- logsMock.mockRestore(); removeMock.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket;
116
+ logsMock.mockRestore();
117
+ removeMock.mockRestore();
118
+ fetchMock.mockRestore();
119
+ globalThis.WebSocket = originalSocket;
82
120
  }
83
121
  });
84
122
 
85
- test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
123
+ test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
86
124
  const core = await import("@solard/core");
87
125
  const { listenTrades } = await import("./live.ts");
88
126
  const mint = "So11111111111111111111111111111111111111112";
89
- const endpoint = "https://migration-listener.example/";
90
- const dir = mkdtempSync(join(tmpdir(), "slrd-sdk-listener-"));
91
- const dbPath = join(dir, "history.sqlite");
127
+ const endpoint = "https://migration-listener.example/";
128
+ const dir = mkdtempSync(join(tmpdir(), "slrd-sdk-listener-"));
129
+ const dbPath = join(dir, "history.sqlite");
92
130
  const originalSocket = globalThis.WebSocket;
93
- globalThis.WebSocket = class extends EventTarget { constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); } close() {} } as any;
94
- const fetchMock = spyOn(globalThis, "fetch").mockImplementation(async () => new Response(JSON.stringify({ result: "ok" })));
95
- let onMigration: any; let onTrade: any;
96
- const tokens = new Set<string>(); const changes: string[] = [];
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[] = [];
97
145
  let closeTrade!: () => void;
98
- const tradeClosed = new Promise<void>(resolve => { closeTrade = resolve; });
99
- const tradeMock = spyOn(core, "subscribeTrades").mockImplementation(async (options: any) => {
100
- options.tokens.forEach((t: string) => tokens.add(t)); onTrade = options.onTrade;
101
- return { addTokens: async (value: any) => { changes.push("add"); (typeof value === "string" ? [value] : value).forEach((t: string) => tokens.add(t)); },
102
- removeTokens: async (value: any) => { changes.push("remove"); (typeof value === "string" ? [value] : value).forEach((t: string) => tokens.delete(t)); },
103
- hasToken: (t: string) => tokens.has(t), listTokens: () => [...tokens], close: async () => closeTrade(), closed: tradeClosed };
104
- });
105
- const migrationMock = spyOn(core, "subscribeMigrations").mockImplementation(async (options: any) => {
106
- onMigration = options.onMigration;
107
- return { mode: "selected", addTokens: async () => {}, removeTokens: async () => {}, hasToken: () => true, listTokens: () => options.tokens, close: async () => {}, closed: Promise.resolve() } as any;
146
+ const tradeClosed = new Promise<void>((resolve) => {
147
+ closeTrade = resolve;
108
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
+ );
109
187
  let listener: Awaited<ReturnType<typeof listenTrades>> | undefined;
110
188
  try {
111
- listener = await listenTrades({ tokens: [mint], rpcUrls: [endpoint], dbPath });
112
- const received: any[] = []; listener.onMigration(event => { received.push(event); });
113
- const event = { signature: "trade", mint, pool: "old-pool", venue: "pumpswap", side: "buy", eventIndex: 0, slot: 11, atMs: Date.now(), baseRaw: 1000000n, quoteRaw: 10000000n,
114
- market: { quoteMint: mint, baseDecimals: 6, quoteDecimals: 9, supplyRaw: 1000000000000000n, baseReserveRaw: 100000000n, quoteReserveRaw: 1000000000n,
115
- priceQuotePerToken: .01, priceSol: .01, priceUsd: 1, marketCapSol: 10000000, marketCapUsd: 1000000000 } };
116
- await onTrade(event); await onTrade(event);
117
- 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 });
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
+ });
118
238
  expect(changes).toEqual(["remove", "add"]);
119
239
  expect(listener.list()).toEqual([mint]);
120
- expect(received[0]).toMatchObject({ oldPool: "old-pool", newPool: "new-pool", slot: 12 });
121
- await listener.close(); await listener.closed;
122
- const repository = new SqliteTokenHistoryRepository(openDatabase(dbPath));
123
- expect(repository.loadTrades(mint)).toHaveLength(1);
124
- expect(repository.loadCandles1s(mint)).toHaveLength(1);
125
- closeDatabase(dbPath);
126
- } finally { await listener?.close(); closeDatabase(dbPath); tradeMock.mockRestore(); migrationMock.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket; rmSync(dir, { recursive: true, force: true }); }
127
- });
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
+ });