@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/README.md +33 -0
- package/package.json +2 -2
- package/src/client.exact-sell.test.ts +95 -0
- package/src/client.listen-trades.test.ts +64 -0
- package/src/client.trade-fees.test.ts +37 -15
- package/src/client.ts +130 -47
- package/src/default.ts +1 -7
- package/src/export-surface.test.ts +41 -20
- package/src/index.ts +43 -40
- package/src/live.test.ts +243 -96
- package/src/live.ts +393 -191
- package/src/operator-api.architecture.test.ts +3 -1
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 {
|
|
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(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
globalThis.WebSocket
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
});
|