@solard/sdk 0.2.33 → 0.2.40

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.
@@ -29,12 +29,22 @@ function exportedNames(value: string) {
29
29
  describe("sdk membrane", () => {
30
30
  test("has an exact curated root export surface", () => {
31
31
  expect(exportedNames(source)).toEqual({
32
- values: [
33
- "normalizeValueBandPolicy", "planValueBandDecision", "valueBandThresholds",
34
- "normalizeTargetWeightPolicy", "planTargetWeightRebalance",
35
- "buildHistoricalCandles", "runHistoricalStrategy", "sweepHistoricalStrategies",
36
- "LAUNCHLAB_PLATFORM_PRESETS", "resolveLaunchLabPlatform",
37
- "createSolard",
32
+ values: [
33
+ "composeTransactionDrafts",
34
+ "observeSolardRpc",
35
+ "withSolardRpcContext",
36
+ "normalizeValueBandPolicy",
37
+ "planValueBandDecision",
38
+ "valueBandThresholds",
39
+ "normalizeTargetWeightPolicy",
40
+ "planTargetWeightRebalance",
41
+ "buildHistoricalCandles",
42
+ "runHistoricalStrategy",
43
+ "sweepHistoricalStrategies",
44
+ "LAUNCHLAB_PLATFORM_PRESETS",
45
+ "resolveLaunchLabPlatform",
46
+ "createSolard",
47
+ "createTraderSolard",
38
48
  "default",
39
49
  "fetchTokenMetadata",
40
50
  "formatRaw",
@@ -48,16 +58,35 @@ describe("sdk membrane", () => {
48
58
  "tokenAmount",
49
59
  ].sort(),
50
60
  types: [
51
- "SellEconomics",
52
- "SupportedLaunchLabPair",
61
+ "TransactionDraft",
62
+ "TransactionGroupInput",
63
+ "PlannedTransaction",
64
+ "SolardRpcObservation",
65
+ "SolardRpcContext",
66
+ "FeedRecoveryVerification",
67
+ "RecoverableTradeSubscription",
68
+ "SellEconomics",
69
+ "SupportedLaunchLabPair",
53
70
  "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",
71
+ "ValueBandPolicy",
72
+ "ValueBandRuntimeState",
73
+ "ValueBandDecision",
74
+ "TargetWeightPolicy",
75
+ "TargetWeightRebalancePlan",
76
+ "HistoricalTradeTape",
77
+ "HistoricalCandle",
78
+ "HistoricalStrategy",
79
+ "HistoricalExecutionOptions",
80
+ "HistoricalStrategyResult",
81
+ "CreatorFeeDiscovery",
82
+ "CreatorFeeGroup",
83
+ "CreatorFeeItem",
84
+ "WalletLedger",
85
+ "WalletLedgerEntry",
86
+ "WalletLedgerOptions",
87
+ "CurrentMarket",
88
+ "PreparedTokenDeployment",
89
+ "PrepareDeploymentArgs",
61
90
  "TradeLandingPolicy",
62
91
  "LiveEndpointOptions",
63
92
  "LivePoolReserves",
@@ -177,7 +206,7 @@ describe("sdk membrane", () => {
177
206
  expect(defaultClient).toContain("instance ??= createSolard()");
178
207
  expect(defaultClient).toContain('if (key === "close")');
179
208
  expect(defaultClient).toContain("getSolUsdPrice");
180
- expect(client).toContain("listenTrades:");
209
+ expect(client).toContain("listenTrades:");
181
210
  expect(defaultClient).toContain("subscribeLaunches");
182
211
  expect(defaultClient).toContain("subscribeMigrations");
183
212
  expect(defaultClient).not.toContain("export default createSolard()");
@@ -202,7 +231,7 @@ describe("sdk membrane", () => {
202
231
 
203
232
  test("does not expose core repositories or secret-bearing handles", () => {
204
233
  expect(client).not.toMatch(
205
- /^(?: {2}| {4})(?:db|wallets|tokens|groups|executions|positions|prices|alts|senders|venues|claimSources|launches|launchpads|connection|signer)\s*:/m,
234
+ /^(?: {2}| {4})(?:db|wallets|tokens|groups|executions|positions|prices|alts|senders|venues|claimSources|launches|launchpads|connection|signer)\s*:/m,
206
235
  );
207
236
  });
208
237
  });
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { default } from "./default.ts";
2
- export { createSolard } from "./client.ts";
2
+ export { createSolard } from "./client.ts";
3
+ export {observeSolardRpc,withSolardRpcContext} from '@solard/core';
4
+ export type {SolardRpcObservation,SolardRpcContext} from '@solard/core';
5
+ export { createTraderSolard } from "@solard/core";
3
6
  export {
4
7
  normalizeValueBandPolicy,
5
8
  planValueBandDecision,
@@ -12,9 +15,9 @@ export {
12
15
  LAUNCHLAB_PLATFORM_PRESETS,
13
16
  resolveLaunchLabPlatform,
14
17
  } from "@solard/core";
15
- export type {
16
- SellEconomics,
17
- SupportedLaunchLabPair,
18
+ export type {
19
+ SellEconomics,
20
+ SupportedLaunchLabPair,
18
21
  ValueBandPolicy,
19
22
  ValueBandRuntimeState,
20
23
  ValueBandDecision,
@@ -122,6 +125,8 @@ export {
122
125
  export type {
123
126
  ListenTradesOptions,
124
127
  LiveEndpointOptions,
128
+ FeedRecoveryVerification,
129
+ RecoverableTradeSubscription,
125
130
  SubscribeLaunchesOptions,
126
131
  SubscribeMigrationsOptions,
127
132
  TradeListener,
@@ -148,3 +153,5 @@ export type {
148
153
  TokenMetadataKind,
149
154
  TokenMetadataMode,
150
155
  } from "@solard/core";
156
+ export {composeTransactionDrafts} from '@solard/core';
157
+ export type {TransactionDraft,TransactionGroupInput,PlannedTransaction} from '@solard/core';
@@ -0,0 +1,122 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { Connection, Keypair, PublicKey } from "@solana/web3.js";
3
+ import { TOKEN_PROGRAM_ID, NATIVE_MINT } from "@solana/spl-token";
4
+ import * as core from "@solard/core";
5
+ import { subscribeTrades } from "./live.ts";
6
+
7
+ test("feed verification gates delivery, validates a snapshot, preserves deduplication and cancels queued recovery", async () => {
8
+ const originalSocket = globalThis.WebSocket;
9
+ globalThis.WebSocket = class extends EventTarget {
10
+ constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); }
11
+ send(text: string) { const value = JSON.parse(text);
12
+ if (value.method === "slotSubscribe") queueMicrotask(() => this.dispatchEvent(
13
+ new MessageEvent("message", { data: JSON.stringify({ id: value.id, result: 1 }) })));
14
+ }
15
+ close() {}
16
+ } as any;
17
+ const fetchMock = spyOn(globalThis, "fetch").mockImplementation((async (_url, init) => {
18
+ const method = JSON.parse(String(init?.body)).method;
19
+ return Response.json({ result: method === "getGenesisHash"
20
+ ? "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d"
21
+ : { context: { slot: 42 }, value: 0 } });
22
+ }) as typeof fetch);
23
+ const mint = Keypair.generate().publicKey, pool = Keypair.generate().publicKey;
24
+ const poolProgram = Keypair.generate().publicKey;
25
+ let snapshotValid = false, emitted = 0, reads = 0;
26
+ const accounts = spyOn(Connection.prototype, "getAccountInfo").mockImplementation(async address => {
27
+ reads++;
28
+ if (!snapshotValid) return null;
29
+ const data = Buffer.alloc(82);
30
+ if (address.equals(mint)) { data[44] = 6; data.writeBigUInt64LE(1_000_000_000n, 36); }
31
+ else { mint.toBuffer().copy(data); NATIVE_MINT.toBuffer().copy(data, 32); }
32
+ return { executable: false, lamports: 1, owner: address.equals(mint) ? TOKEN_PROGRAM_ID : poolProgram, data };
33
+ });
34
+ let emit!: (event: core.TradeEvent) => Promise<void>;
35
+ const trades = spyOn(core, "subscribeTrades").mockImplementation(async options => {
36
+ emit = options.onTrade as typeof emit;
37
+ const watched = new Set(options.tokens);
38
+ let settle!: () => void;
39
+ const closed = new Promise<void>(resolve => { settle = resolve; });
40
+ return {
41
+ addTokens: async values => { for (const value of typeof values === "string" ? [values] : values) watched.add(value); },
42
+ removeTokens: async values => { for (const value of typeof values === "string" ? [values] : values) watched.delete(value); },
43
+ hasToken: value => watched.has(value), listTokens: () => [...watched],
44
+ close: async () => { watched.clear(); settle(); }, closed,
45
+ };
46
+ });
47
+ let subscription: Awaited<ReturnType<typeof subscribeTrades>> | undefined;
48
+ let clock: ReturnType<typeof spyOn> | undefined;
49
+ const event = (signature: string, atMs = Date.now()) => ({
50
+ type: "trade", mint: mint.toBase58(), signature, venue: "pumpswap", pool: pool.toBase58(),
51
+ side: "buy", slot: 42, atMs, baseRaw: 1n, quoteRaw: 2n,
52
+ metadata: null, market: {
53
+ quoteMint: NATIVE_MINT.toBase58(), baseDecimals: 6, quoteDecimals: 9,
54
+ supplyRaw: 1_000_000_000n, supply: 1000,
55
+ baseReserveRaw: 1n, baseReserve: 0.000001,
56
+ quoteReserveRaw: 2n, quoteReserve: 0.000000002,
57
+ priceQuotePerToken: 0.002, marketCapQuote: 2,
58
+ priceSol: 0.002, marketCapSol: 2, solUsd: null,
59
+ solUsdSource: null, solUsdAtMs: null, priceUsd: null, marketCapUsd: null,
60
+ },
61
+ }) as core.TradeEvent;
62
+ const verify = async ({ connection, tokens, signal }: coreVerification) => {
63
+ expect(signal.aborted).toBe(false);
64
+ expect(tokens).toContain(mint.toBase58());
65
+ const [mintAccount, poolAccount] = await Promise.all([
66
+ connection.getAccountInfo(mint), connection.getAccountInfo(pool),
67
+ ]);
68
+ return !!mintAccount && !!poolAccount && mintAccount.owner.equals(TOKEN_PROGRAM_ID) &&
69
+ mintAccount.data[44] === 6 && mintAccount.data.readBigUInt64LE(36) > 0n &&
70
+ poolAccount.owner.equals(poolProgram) &&
71
+ new PublicKey(poolAccount.data.subarray(0, 32)).equals(mint) &&
72
+ new PublicKey(poolAccount.data.subarray(32, 64)).equals(NATIVE_MINT);
73
+ };
74
+ try {
75
+ // Environment-only/single-endpoint streams expose the same recovery API.
76
+ const oldEndpoint = process.env.RPC_ENDPOINT;
77
+ process.env.RPC_ENDPOINT = "https://recovery-only.example/";
78
+ try { subscription = await subscribeTrades({ tokens: [mint.toBase58()],
79
+ rpcUrls: [], healthCheckIntervalMs: 1000, onTrade: () => { emitted++; } }); }
80
+ finally { if (oldEndpoint === undefined) delete process.env.RPC_ENDPOINT; else process.env.RPC_ENDPOINT = oldEndpoint; }
81
+ expect(await subscription.verifyAndResume(verify)).toBe(false);
82
+ await emit(event("blocked")); expect(emitted).toBe(0);
83
+ snapshotValid = true;
84
+ expect(await subscription.verifyAndResume(verify)).toBe(true);
85
+ expect(reads).toBe(4);
86
+ await emit(event("accepted")); await emit(event("accepted"));
87
+ await emit(event("old", Date.now() - 31_000));
88
+ expect(emitted).toBe(1);
89
+ const future = Date.now() + 61_000;
90
+ clock = spyOn(Date, "now").mockReturnValue(future);
91
+ expect(subscription.feedVerified).toBe(false);
92
+ await emit(event("after-silence")); expect(emitted).toBe(1);
93
+ expect(await subscription.verifyAndResume(verify)).toBe(true);
94
+ await emit(event("resumed")); expect(emitted).toBe(2);
95
+ clock.mockRestore(); clock = undefined;
96
+ let finish!: (value: boolean) => void;
97
+ const verification = subscription.verifyAndResume(async () => await new Promise<boolean>(resolve => { finish = resolve; }));
98
+ while (!finish) await Bun.sleep(1);
99
+ const added = subscription.addTokens(Keypair.generate().publicKey.toBase58());
100
+ finish(true);
101
+ expect(await verification).toBe(false); await added;
102
+ expect(subscription.feedVerified).toBe(false);
103
+ let queuedRan = false;
104
+ let hangingStarted = false;
105
+ const hanging = subscription.verifyAndResume(async () => {
106
+ hangingStarted = true; return await new Promise<boolean>(() => {});
107
+ });
108
+ const hangingResult = hanging.then(value => ({ value, error: undefined as unknown }),
109
+ error => ({ value: false, error }));
110
+ while (!hangingStarted) await Bun.sleep(1);
111
+ await expect(subscription.verifyAndResume(async () => { queuedRan = true; return true; }, 5))
112
+ .rejects.toThrow("cancelled or timed out");
113
+ expect(queuedRan).toBe(false);
114
+ await subscription.close();
115
+ expect(((await hangingResult).error as Error).message).toContain("cancelled or timed out");
116
+ expect(subscription.feedVerified).toBe(false);
117
+ } finally {
118
+ clock?.mockRestore(); await subscription?.close();
119
+ trades.mockRestore(); accounts.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket;
120
+ }
121
+ });
122
+ type coreVerification = Parameters<import("./live.ts").FeedRecoveryVerification>[0];
package/src/live.test.ts CHANGED
@@ -1,11 +1,18 @@
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
+ const mockFetch = (implementation: (...args: Parameters<typeof fetch>) => ReturnType<typeof fetch>) =>
3
+ spyOn(globalThis, "fetch").mockImplementation(implementation as typeof fetch);
4
+ import { Connection } from "@solana/web3.js";
5
+ import { mkdtempSync, rmSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { openDatabase, closeDatabase } from "../../core/src/db/database.ts";
9
+ import { SqliteTokenHistoryRepository } from "../../core/src/chain/token-history/repository.ts";
10
+ import {
11
+ deriveWebSocketEndpoint,
12
+ LiveEndpointPool,
13
+ redactRpcEndpoint,
14
+ subscribeTrades,
15
+ } from "./live.ts";
9
16
 
10
17
  describe("sdk live endpoint", () => {
11
18
  test("derives websocket from the one RPC endpoint", () => {
@@ -21,16 +28,23 @@ describe("sdk live endpoint", () => {
21
28
  redactRpcEndpoint(
22
29
  "https://mainnet.helius-rpc.com/?api-key=secret-value&foo=bar",
23
30
  ),
24
- ).toBe("https://mainnet.helius-rpc.com/?api-key=<redacted>&foo=bar");
31
+ ).toBe("https://mainnet.helius-rpc.com/***");
25
32
  });
26
33
  });
27
34
  test("live endpoint selection requires readiness, sticks, and rotates after later failures", async () => {
28
35
  const calls: string[] = [];
29
36
  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
- });
37
+ const pool = new LiveEndpointPool(
38
+ [
39
+ "https://first.example/",
40
+ "https://second.example/",
41
+ "https://public.example/",
42
+ ],
43
+ async (endpoint) => {
44
+ calls.push(endpoint);
45
+ if (unavailable.has(endpoint)) throw new Error("probe failed");
46
+ },
47
+ );
34
48
  expect(await pool.select()).toBe("https://second.example/");
35
49
  calls.length = 0;
36
50
  expect(await pool.select()).toBe("https://second.example/");
@@ -45,83 +59,245 @@ test("pooled trade stream preserves dynamic watched tokens when it rotates and c
45
59
  const originalSocket = globalThis.WebSocket;
46
60
  let firstHealthy = true;
47
61
  let nextId = 0;
48
- const registrations: Array<{ endpoint: string; token: string }> = [];
62
+ const registrations: Array<{ endpoint: string; ws: string; token: string }> = [];
49
63
  const first = "https://live-primary.example/";
50
64
  const second = "https://live-fallback.example/";
51
65
  const mint = "So11111111111111111111111111111111111111112";
52
66
  globalThis.WebSocket = class extends EventTarget {
53
- constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); }
67
+ constructor() {
68
+ super();
69
+ queueMicrotask(() => this.dispatchEvent(new Event("open")));
70
+ }
71
+ send(payload: string) {
72
+ const request = JSON.parse(payload) as { method?: string; id?: number };
73
+ if (request.method === "slotSubscribe") queueMicrotask(() =>
74
+ this.dispatchEvent(new MessageEvent("message", { data: JSON.stringify({
75
+ jsonrpc: "2.0", id: request.id, result: 42,
76
+ }) })));
77
+ }
54
78
  close() {}
55
79
  } 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);
80
+ const fetchMock = mockFetch(
81
+ async (input, init) => {
82
+ if (String(input) === first && !firstHealthy)
83
+ return new Response("", { status: 401 });
84
+ const method = JSON.parse(String(init?.body)).method;
85
+ return new Response(JSON.stringify({ result: method === "getGenesisHash"
86
+ ? "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d"
87
+ : { context: { slot: 50 }, value: 0 } }));
88
+ },
89
+ );
90
+ const logsMock = spyOn(Connection.prototype, "onLogs").mockImplementation(
91
+ function (token) {
92
+ registrations.push({ endpoint: this.rpcEndpoint,
93
+ ws: (this as any)._rpcWsEndpoint, token: String(token) });
94
+ return ++nextId;
95
+ },
96
+ );
97
+ const removeMock = spyOn(
98
+ Connection.prototype,
99
+ "removeOnLogsListener",
100
+ ).mockResolvedValue(undefined);
65
101
  let subscription: Awaited<ReturnType<typeof subscribeTrades>> | undefined;
66
102
  try {
67
- subscription = await subscribeTrades({ tokens: [], rpcUrls: [first, second], healthCheckIntervalMs: 1000, onTrade() {} });
103
+ subscription = await subscribeTrades({
104
+ tokens: [],
105
+ rpcUrls: [first, second],
106
+ healthCheckIntervalMs: 1000,
107
+ onTrade() {},
108
+ });
68
109
  await subscription.addTokens(mint);
69
110
  firstHealthy = false;
70
111
  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 }]);
112
+ while (
113
+ !registrations.some((row) => row.endpoint === second) &&
114
+ Date.now() < deadline
115
+ )
116
+ await Bun.sleep(25);
117
+ expect(registrations).toEqual([
118
+ { endpoint: first, ws: "wss://live-primary.example/", token: mint },
119
+ { endpoint: second, ws: "wss://live-fallback.example/", token: mint },
120
+ ]);
73
121
  expect(subscription.listTokens()).toEqual([mint]);
74
122
  expect(subscription.hasToken(mint)).toBe(true);
123
+ // A provider change must be explicitly reconciled by the application;
124
+ // a rejected snapshot cannot re-enable trading callbacks.
125
+ expect(subscription.feedVerified).toBe(false);
126
+ const rejected = await subscription.verifyAndResume(async ({tokens}) => {
127
+ expect(tokens).toEqual([mint]); return false;
128
+ });
129
+ expect(rejected).toBe(false);
130
+ expect(subscription.feedVerified).toBe(false);
131
+ const recovery = await subscription.verifyAndResume(async ({connection, tokens, signal}) => {
132
+ expect(connection.rpcEndpoint).toBe(second);
133
+ expect(tokens).toEqual([mint]);
134
+ expect(signal.aborted).toBe(false);
135
+ return true; // fixture represents successfully checked fresh pool/mint state
136
+ });
137
+ expect(recovery).toBe(true);
138
+ expect(subscription.feedVerified).toBe(true);
139
+ await expect(subscription.verifyAndResume(async () => new Promise<boolean>(() => {}), 5))
140
+ .rejects.toThrow("Feed verification cancelled or timed out");
75
141
  await subscription.close();
76
142
  await subscription.closed;
77
143
  expect(subscription.listTokens()).toEqual([]);
78
144
  expect(removeMock).toHaveBeenCalledTimes(2);
79
145
  } finally {
80
146
  await subscription?.close();
81
- logsMock.mockRestore(); removeMock.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket;
147
+ logsMock.mockRestore();
148
+ removeMock.mockRestore();
149
+ fetchMock.mockRestore();
150
+ globalThis.WebSocket = originalSocket;
82
151
  }
83
152
  });
84
153
 
85
- test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
154
+ test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
86
155
  const core = await import("@solard/core");
87
156
  const { listenTrades } = await import("./live.ts");
88
157
  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");
158
+ const endpoint = "https://migration-listener.example/";
159
+ const dir = mkdtempSync(join(tmpdir(), "slrd-sdk-listener-"));
160
+ const dbPath = join(dir, "history.sqlite");
92
161
  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[] = [];
162
+ globalThis.WebSocket = class extends EventTarget {
163
+ constructor() {
164
+ super();
165
+ queueMicrotask(() => this.dispatchEvent(new Event("open")));
166
+ }
167
+ send(payload: string) {
168
+ const request = JSON.parse(payload) as { method?: string; id?: number };
169
+ if (request.method === "slotSubscribe") queueMicrotask(() =>
170
+ this.dispatchEvent(new MessageEvent("message", { data: JSON.stringify({
171
+ jsonrpc: "2.0", id: request.id, result: 42,
172
+ }) })));
173
+ }
174
+ close() {}
175
+ } as any;
176
+ const fetchMock = mockFetch(
177
+ async (_input, init) => {
178
+ const method = JSON.parse(String(init?.body)).method;
179
+ return new Response(JSON.stringify({ result: method === "getGenesisHash"
180
+ ? "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d"
181
+ : { context: { slot: 50 }, value: 0 } }));
182
+ },
183
+ );
184
+ let onMigration: any;
185
+ let onTrade: any;
186
+ const tokens = new Set<string>();
187
+ const changes: string[] = [];
97
188
  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;
189
+ const tradeClosed = new Promise<void>((resolve) => {
190
+ closeTrade = resolve;
108
191
  });
192
+ const tradeMock = spyOn(core, "subscribeTrades").mockImplementation(
193
+ async (options: any) => {
194
+ options.tokens.forEach((t: string) => tokens.add(t));
195
+ onTrade = options.onTrade;
196
+ return {
197
+ addTokens: async (value: any) => {
198
+ changes.push("add");
199
+ (typeof value === "string" ? [value] : value).forEach((t: string) =>
200
+ tokens.add(t),
201
+ );
202
+ },
203
+ removeTokens: async (value: any) => {
204
+ changes.push("remove");
205
+ (typeof value === "string" ? [value] : value).forEach((t: string) =>
206
+ tokens.delete(t),
207
+ );
208
+ },
209
+ hasToken: (t: string) => tokens.has(t),
210
+ listTokens: () => [...tokens],
211
+ close: async () => closeTrade(),
212
+ closed: tradeClosed,
213
+ };
214
+ },
215
+ );
216
+ const migrationMock = spyOn(core, "subscribeMigrations").mockImplementation(
217
+ async (options: any) => {
218
+ onMigration = options.onMigration;
219
+ return {
220
+ mode: "selected",
221
+ addTokens: async () => {},
222
+ removeTokens: async () => {},
223
+ hasToken: () => true,
224
+ listTokens: () => options.tokens,
225
+ close: async () => {},
226
+ closed: Promise.resolve(),
227
+ } as any;
228
+ },
229
+ );
109
230
  let listener: Awaited<ReturnType<typeof listenTrades>> | undefined;
110
231
  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 });
232
+ listener = await listenTrades({
233
+ tokens: [mint],
234
+ rpcUrls: [endpoint],
235
+ dbPath,
236
+ });
237
+ const received: any[] = [];
238
+ listener.onMigration((event) => {
239
+ received.push(event);
240
+ });
241
+ const event = {
242
+ signature: "trade",
243
+ mint,
244
+ pool: "old-pool",
245
+ venue: "pumpswap",
246
+ side: "buy",
247
+ eventIndex: 0,
248
+ slot: 11,
249
+ atMs: Date.now(),
250
+ baseRaw: 1000000n,
251
+ quoteRaw: 10000000n,
252
+ market: {
253
+ quoteMint: mint,
254
+ baseDecimals: 6,
255
+ quoteDecimals: 9,
256
+ supplyRaw: 1000000000000000n,
257
+ baseReserveRaw: 100000000n,
258
+ quoteReserveRaw: 1000000000n,
259
+ priceQuotePerToken: 0.01,
260
+ priceSol: 0.01,
261
+ priceUsd: 1,
262
+ marketCapSol: 10000000,
263
+ marketCapUsd: 1000000000,
264
+ },
265
+ };
266
+ await onTrade(event);
267
+ await onTrade(event);
268
+ await onMigration({
269
+ type: "migration",
270
+ mint,
271
+ pool: "new-pool",
272
+ signature: "migration",
273
+ slot: 12,
274
+ venue: "pump",
275
+ destination: "pumpswap",
276
+ quoteMint: mint,
277
+ atMs: Date.now(),
278
+ isMayhemMode: false,
279
+ metadata: null,
280
+ });
118
281
  expect(changes).toEqual(["remove", "add"]);
119
282
  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
- });
283
+ expect(received[0]).toMatchObject({
284
+ oldPool: "old-pool",
285
+ newPool: "new-pool",
286
+ slot: 12,
287
+ });
288
+ await listener.close();
289
+ await listener.closed;
290
+ const repository = new SqliteTokenHistoryRepository(openDatabase(dbPath));
291
+ expect(repository.loadTrades(mint)).toHaveLength(1);
292
+ expect(repository.loadCandles1s(mint)).toHaveLength(1);
293
+ closeDatabase(dbPath);
294
+ } finally {
295
+ await listener?.close();
296
+ closeDatabase(dbPath);
297
+ tradeMock.mockRestore();
298
+ migrationMock.mockRestore();
299
+ fetchMock.mockRestore();
300
+ globalThis.WebSocket = originalSocket;
301
+ rmSync(dir, { recursive: true, force: true });
302
+ }
303
+ });