@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.
- package/README.md +34 -34
- package/package.json +2 -2
- package/src/client.exact-sell.test.ts +95 -45
- package/src/client.listen-trades.test.ts +64 -34
- package/src/client.trade-fees.test.ts +37 -15
- package/src/client.ts +106 -48
- package/src/default.ts +2 -5
- package/src/export-surface.test.ts +36 -17
- package/src/index.ts +5 -4
- package/src/live.test.ts +191 -58
- package/src/live.ts +268 -83
- package/src/operator-api.architecture.test.ts +3 -1
|
@@ -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",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
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",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
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 {
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
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() {
|
|
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(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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({
|
|
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 (
|
|
72
|
-
|
|
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();
|
|
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 {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 => {
|
|
99
|
-
|
|
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({
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
});
|