@solard/sdk 0.2.27 → 0.2.31
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/README.md +21 -6
- package/package.json +2 -2
- package/src/client.trade-fees.test.ts +60 -0
- package/src/client.ts +263 -280
- package/src/default.ts +51 -51
- package/src/export-surface.test.ts +204 -188
- package/src/index.ts +142 -100
- package/src/live.test.ts +108 -15
- package/src/live.ts +468 -307
- package/src/operator-api.architecture.test.ts +45 -44
package/src/live.test.ts
CHANGED
|
@@ -1,20 +1,113 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
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";
|
|
4
|
+
|
|
5
|
+
describe("sdk live endpoint", () => {
|
|
6
|
+
test("derives websocket from the one RPC endpoint", () => {
|
|
7
|
+
expect(
|
|
8
|
+
deriveWebSocketEndpoint(
|
|
9
|
+
"https://mainnet.helius-rpc.com/?api-key=secret-value",
|
|
10
|
+
),
|
|
11
|
+
).toBe("wss://mainnet.helius-rpc.com/?api-key=secret-value");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("redacts API credentials in diagnostics", () => {
|
|
15
|
+
expect(
|
|
16
|
+
redactRpcEndpoint(
|
|
17
|
+
"https://mainnet.helius-rpc.com/?api-key=secret-value&foo=bar",
|
|
18
|
+
),
|
|
19
|
+
).toBe("https://mainnet.helius-rpc.com/?api-key=<redacted>&foo=bar");
|
|
20
|
+
});
|
|
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
|
+
});
|
|
3
38
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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" }));
|
|
11
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
|
+
});
|
|
12
79
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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;
|
|
19
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; }
|
|
20
113
|
});
|