@solard/sdk 0.2.29 → 0.2.33
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 +54 -6
- package/package.json +2 -2
- package/src/client.exact-sell.test.ts +45 -0
- package/src/client.listen-trades.test.ts +34 -0
- package/src/client.trade-fees.test.ts +60 -60
- package/src/client.ts +96 -82
- package/src/default.ts +46 -49
- package/src/export-surface.test.ts +207 -191
- package/src/index.ts +41 -3
- package/src/live.test.ts +126 -19
- package/src/live.ts +506 -328
- package/src/operator-api.architecture.test.ts +45 -44
|
@@ -1,192 +1,208 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
const source = readFileSync(join(import.meta.dir, "index.ts"), "utf8");
|
|
6
|
-
const client = readFileSync(join(import.meta.dir, "client.ts"), "utf8");
|
|
7
|
-
const protocol = readFileSync(join(import.meta.dir, "protocol.ts"), "utf8");
|
|
8
|
-
const defaultClient = readFileSync(join(import.meta.dir, "default.ts"), "utf8");
|
|
9
|
-
const live = readFileSync(join(import.meta.dir, "live.ts"), "utf8");
|
|
10
|
-
|
|
11
|
-
function exportedNames(value: string) {
|
|
12
|
-
const values: string[] = [];
|
|
13
|
-
const types: string[] = [];
|
|
14
|
-
const pattern = /export\s+(type\s+)?\{([\s\S]*?)\}\s+from\s+["'][^"']+["'];/g;
|
|
15
|
-
for (const match of value.matchAll(pattern)) {
|
|
16
|
-
const target = match[1] ? types : values;
|
|
17
|
-
for (const raw of match[2]!.split(",")) {
|
|
18
|
-
const name =
|
|
19
|
-
raw.trim().split(/\s+as\s+/)[1] ?? raw.trim().split(/\s+as\s+/)[0];
|
|
20
|
-
if (name) target.push(name.trim());
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
values: [...new Set(values)].sort(),
|
|
25
|
-
types: [...new Set(types)].sort(),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("sdk membrane", () => {
|
|
30
|
-
test("has an exact curated root export surface", () => {
|
|
31
|
-
expect(exportedNames(source)).toEqual({
|
|
32
|
-
values: [
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const source = readFileSync(join(import.meta.dir, "index.ts"), "utf8");
|
|
6
|
+
const client = readFileSync(join(import.meta.dir, "client.ts"), "utf8");
|
|
7
|
+
const protocol = readFileSync(join(import.meta.dir, "protocol.ts"), "utf8");
|
|
8
|
+
const defaultClient = readFileSync(join(import.meta.dir, "default.ts"), "utf8");
|
|
9
|
+
const live = readFileSync(join(import.meta.dir, "live.ts"), "utf8");
|
|
10
|
+
|
|
11
|
+
function exportedNames(value: string) {
|
|
12
|
+
const values: string[] = [];
|
|
13
|
+
const types: string[] = [];
|
|
14
|
+
const pattern = /export\s+(type\s+)?\{([\s\S]*?)\}\s+from\s+["'][^"']+["'];/g;
|
|
15
|
+
for (const match of value.matchAll(pattern)) {
|
|
16
|
+
const target = match[1] ? types : values;
|
|
17
|
+
for (const raw of match[2]!.split(",")) {
|
|
18
|
+
const name =
|
|
19
|
+
raw.trim().split(/\s+as\s+/)[1] ?? raw.trim().split(/\s+as\s+/)[0];
|
|
20
|
+
if (name) target.push(name.trim());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
values: [...new Set(values)].sort(),
|
|
25
|
+
types: [...new Set(types)].sort(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("sdk membrane", () => {
|
|
30
|
+
test("has an exact curated root export surface", () => {
|
|
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",
|
|
38
|
+
"default",
|
|
39
|
+
"fetchTokenMetadata",
|
|
40
|
+
"formatRaw",
|
|
41
|
+
"getSolUsdPrice",
|
|
42
|
+
"isDefinitivePreSubmissionError",
|
|
43
|
+
"publicTokenMetadataUrl",
|
|
44
|
+
"subscribeLaunches",
|
|
45
|
+
"subscribeMigrations",
|
|
46
|
+
"listenTrades",
|
|
47
|
+
"sol",
|
|
48
|
+
"tokenAmount",
|
|
49
|
+
].sort(),
|
|
46
50
|
types: [
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
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
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
expect(
|
|
177
|
-
expect(defaultClient).
|
|
178
|
-
expect(
|
|
179
|
-
|
|
180
|
-
);
|
|
181
|
-
expect(
|
|
182
|
-
|
|
183
|
-
);
|
|
184
|
-
expect(
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("does not
|
|
188
|
-
expect(
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
51
|
+
"SellEconomics",
|
|
52
|
+
"SupportedLaunchLabPair",
|
|
53
|
+
"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
|
+
"TradeLandingPolicy",
|
|
62
|
+
"LiveEndpointOptions",
|
|
63
|
+
"LivePoolReserves",
|
|
64
|
+
"ClaimCreatorRewardsOptions",
|
|
65
|
+
"CreatorRewardClaimPayout",
|
|
66
|
+
"CreatorRewardClaimResult",
|
|
67
|
+
"CumulativeDistributionExecuteOptions",
|
|
68
|
+
"CumulativeDistributionInput",
|
|
69
|
+
"CumulativeDistributionPlan",
|
|
70
|
+
"CumulativeDistributionRecipient",
|
|
71
|
+
"CumulativeDistributionState",
|
|
72
|
+
"CumulativeEntitlement",
|
|
73
|
+
"FetchTokenMetadataOptions",
|
|
74
|
+
"GetSolUsdPriceOptions",
|
|
75
|
+
"HumanAmount",
|
|
76
|
+
"LaunchEvent",
|
|
77
|
+
"LaunchSubscription",
|
|
78
|
+
"LaunchVenue",
|
|
79
|
+
"MigrationDestination",
|
|
80
|
+
"MigrationEvent",
|
|
81
|
+
"MigrationSubscription",
|
|
82
|
+
"MigrationVenue",
|
|
83
|
+
"MarketHistory",
|
|
84
|
+
"MarketHistoryOptions",
|
|
85
|
+
"MarketPrice",
|
|
86
|
+
"SolUsdPrice",
|
|
87
|
+
"SolUsdSource",
|
|
88
|
+
"SubscribeLaunchesOptions",
|
|
89
|
+
"SubscribeMigrationsOptions",
|
|
90
|
+
"ListenTradesOptions",
|
|
91
|
+
"MergedReplayEventStream",
|
|
92
|
+
"QuoteAsset",
|
|
93
|
+
"ReplayCoverage",
|
|
94
|
+
"ReplayEventSubscription",
|
|
95
|
+
"ReplayEventsOptions",
|
|
96
|
+
"ReplayHistory",
|
|
97
|
+
"ReplayItem",
|
|
98
|
+
"ReplayOptions",
|
|
99
|
+
"ReplayPayout",
|
|
100
|
+
"ReplayTransaction",
|
|
101
|
+
"RecordConfirmedTradeInput",
|
|
102
|
+
"SendReceipt",
|
|
103
|
+
"SenderId",
|
|
104
|
+
"SimulationResult",
|
|
105
|
+
"SolardDecodedTransaction",
|
|
106
|
+
"SolardPosition",
|
|
107
|
+
"SolardPositionQuery",
|
|
108
|
+
"SolardTransactionNativeBalance",
|
|
109
|
+
"SolardTransactionOptions",
|
|
110
|
+
"SolardTransactionTokenBalance",
|
|
111
|
+
"SolardTrade",
|
|
112
|
+
"SolardTradeQuery",
|
|
113
|
+
"SolardTradeSide",
|
|
114
|
+
"SolardTradeStatus",
|
|
115
|
+
"TradeEvent",
|
|
116
|
+
"TradeMarket",
|
|
117
|
+
"TradeSide",
|
|
118
|
+
"TradeListener",
|
|
119
|
+
"TradeVenue",
|
|
120
|
+
"Solard",
|
|
121
|
+
"SolardBuyInput",
|
|
122
|
+
"SolardSellInput",
|
|
123
|
+
"SolardTradeExecutionOptions",
|
|
124
|
+
"SolardTradeExecutionResult",
|
|
125
|
+
"SolardCanonicalEvent",
|
|
126
|
+
"SolardClaimAttribution",
|
|
127
|
+
"SolardClaimEvent",
|
|
128
|
+
"SolardEventsApi",
|
|
129
|
+
"SolardHistoryApi",
|
|
130
|
+
"SolardOptions",
|
|
131
|
+
"ExcludedTokenHolder",
|
|
132
|
+
"TokenHolder",
|
|
133
|
+
"TokenHolderSnapshot",
|
|
134
|
+
"TokenHolderSnapshotOptions",
|
|
135
|
+
"TokenEventHistoryProgress",
|
|
136
|
+
"TokenMetadata",
|
|
137
|
+
"TokenMetadataHint",
|
|
138
|
+
"TokenMetadataKind",
|
|
139
|
+
"TokenMetadataMode",
|
|
140
|
+
"TokenRef",
|
|
141
|
+
"TokenRow",
|
|
142
|
+
"WalletInfo",
|
|
143
|
+
"WalletPrivateKeyExport",
|
|
144
|
+
"WalletPrivateKeyFormat",
|
|
145
|
+
"WalletRef",
|
|
146
|
+
].sort(),
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("does not wildcard-export or deep-import core", () => {
|
|
151
|
+
expect(source).not.toContain('export * from "@solard/core"');
|
|
152
|
+
expect(source).not.toMatch(/\.\.\/\.\.\/core\/src|@solard\/core\//);
|
|
153
|
+
expect(client).not.toMatch(/\.\.\/\.\.\/core\/src|@solard\/core\//);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("keeps raw protocol decoders off the root surface", () => {
|
|
157
|
+
expect(source).not.toMatch(
|
|
158
|
+
/decode(?:ProgramDataLogs|PumpProgramData|PumpSwapProgramData|RaydiumLaunchLabProgramData)/,
|
|
159
|
+
);
|
|
160
|
+
expect(exportedNames(protocol)).toEqual({
|
|
161
|
+
values: [
|
|
162
|
+
"decodeProgramDataLogs",
|
|
163
|
+
"decodePumpProgramData",
|
|
164
|
+
"decodePumpSwapProgramData",
|
|
165
|
+
"decodeRaydiumLaunchLabProgramData",
|
|
166
|
+
].sort(),
|
|
167
|
+
types: [
|
|
168
|
+
"PumpDecodedEvent",
|
|
169
|
+
"PumpSwapDecodedEvent",
|
|
170
|
+
"RaydiumLaunchLabDecodedEvent",
|
|
171
|
+
].sort(),
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("exports a lazy zero-config default client", () => {
|
|
176
|
+
expect(source).toContain('export { default } from "./default.ts"');
|
|
177
|
+
expect(defaultClient).toContain("instance ??= createSolard()");
|
|
178
|
+
expect(defaultClient).toContain('if (key === "close")');
|
|
179
|
+
expect(defaultClient).toContain("getSolUsdPrice");
|
|
180
|
+
expect(client).toContain("listenTrades:");
|
|
181
|
+
expect(defaultClient).toContain("subscribeLaunches");
|
|
182
|
+
expect(defaultClient).toContain("subscribeMigrations");
|
|
183
|
+
expect(defaultClient).not.toContain("export default createSolard()");
|
|
184
|
+
expect(live).toContain("process.env.RPC_ENDPOINT");
|
|
185
|
+
expect(live).not.toContain("SOLANA_RPC_URL");
|
|
186
|
+
expect(live).not.toContain("HELIUS_RPC_URL");
|
|
187
|
+
expect(live).not.toContain("SOLANA_WS_URL");
|
|
188
|
+
expect(live).not.toContain("HELIUS_WS_URL");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("does not create persistence at module import", () => {
|
|
192
|
+
expect(source).not.toMatch(/createCoreSolard|createTraderSolard\(\)/);
|
|
193
|
+
expect(defaultClient).not.toMatch(/^const\s+\w+\s*=\s*createSolard\(\)/m);
|
|
194
|
+
expect(client.indexOf("createCoreSolard(options)")).toBeGreaterThan(
|
|
195
|
+
client.indexOf("export function createSolard"),
|
|
196
|
+
);
|
|
197
|
+
expect(client).toContain(
|
|
198
|
+
"recordConfirmedTrade: core.recordConfirmedTrade.bind(core)",
|
|
199
|
+
);
|
|
200
|
+
expect(client).toContain("trades: core.trades.bind(core)");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("does not expose core repositories or secret-bearing handles", () => {
|
|
204
|
+
expect(client).not.toMatch(
|
|
205
|
+
/^(?: {2}| {4})(?:db|wallets|tokens|groups|executions|positions|prices|alts|senders|venues|claimSources|launches|launchpads|connection|signer)\s*:/m,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
export { default } from "./default.ts";
|
|
2
2
|
export { createSolard } from "./client.ts";
|
|
3
|
+
export {
|
|
4
|
+
normalizeValueBandPolicy,
|
|
5
|
+
planValueBandDecision,
|
|
6
|
+
valueBandThresholds,
|
|
7
|
+
normalizeTargetWeightPolicy,
|
|
8
|
+
planTargetWeightRebalance,
|
|
9
|
+
buildHistoricalCandles,
|
|
10
|
+
runHistoricalStrategy,
|
|
11
|
+
sweepHistoricalStrategies,
|
|
12
|
+
LAUNCHLAB_PLATFORM_PRESETS,
|
|
13
|
+
resolveLaunchLabPlatform,
|
|
14
|
+
} from "@solard/core";
|
|
3
15
|
export type {
|
|
16
|
+
SellEconomics,
|
|
17
|
+
SupportedLaunchLabPair,
|
|
18
|
+
ValueBandPolicy,
|
|
19
|
+
ValueBandRuntimeState,
|
|
20
|
+
ValueBandDecision,
|
|
21
|
+
TargetWeightPolicy,
|
|
22
|
+
TargetWeightRebalancePlan,
|
|
23
|
+
HistoricalTradeTape,
|
|
24
|
+
HistoricalCandle,
|
|
25
|
+
HistoricalStrategy,
|
|
26
|
+
HistoricalExecutionOptions,
|
|
27
|
+
HistoricalStrategyResult,
|
|
28
|
+
CreatorFeeDiscovery,
|
|
29
|
+
CreatorFeeGroup,
|
|
30
|
+
CreatorFeeItem,
|
|
31
|
+
WalletLedger,
|
|
32
|
+
WalletLedgerEntry,
|
|
33
|
+
WalletLedgerOptions,
|
|
34
|
+
CurrentMarket,
|
|
35
|
+
PreparedTokenDeployment,
|
|
36
|
+
PrepareDeploymentArgs,
|
|
37
|
+
} from "@solard/core";
|
|
38
|
+
export type {
|
|
4
39
|
Solard,
|
|
5
40
|
SolardBuyInput,
|
|
6
41
|
SolardEventsApi,
|
|
@@ -18,9 +53,10 @@ export {
|
|
|
18
53
|
isDefinitivePreSubmissionError,
|
|
19
54
|
} from "@solard/core";
|
|
20
55
|
|
|
21
|
-
export type {
|
|
22
|
-
ClaimCreatorRewardsOptions,
|
|
23
|
-
TradeLandingPolicy,
|
|
56
|
+
export type {
|
|
57
|
+
ClaimCreatorRewardsOptions,
|
|
58
|
+
TradeLandingPolicy,
|
|
59
|
+
LivePoolReserves,
|
|
24
60
|
CreatorRewardClaimPayout,
|
|
25
61
|
CreatorRewardClaimResult,
|
|
26
62
|
CumulativeDistributionExecuteOptions,
|
|
@@ -85,9 +121,11 @@ export {
|
|
|
85
121
|
} from "./live.ts";
|
|
86
122
|
export type {
|
|
87
123
|
ListenTradesOptions,
|
|
124
|
+
LiveEndpointOptions,
|
|
88
125
|
SubscribeLaunchesOptions,
|
|
89
126
|
SubscribeMigrationsOptions,
|
|
90
127
|
TradeListener,
|
|
128
|
+
TradeListenerMigration,
|
|
91
129
|
} from "./live.ts";
|
|
92
130
|
export type {
|
|
93
131
|
LaunchEvent,
|
package/src/live.test.ts
CHANGED
|
@@ -1,20 +1,127 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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";
|
|
9
|
+
|
|
10
|
+
describe("sdk live endpoint", () => {
|
|
11
|
+
test("derives websocket from the one RPC endpoint", () => {
|
|
12
|
+
expect(
|
|
13
|
+
deriveWebSocketEndpoint(
|
|
14
|
+
"https://mainnet.helius-rpc.com/?api-key=secret-value",
|
|
15
|
+
),
|
|
16
|
+
).toBe("wss://mainnet.helius-rpc.com/?api-key=secret-value");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("redacts API credentials in diagnostics", () => {
|
|
20
|
+
expect(
|
|
21
|
+
redactRpcEndpoint(
|
|
22
|
+
"https://mainnet.helius-rpc.com/?api-key=secret-value&foo=bar",
|
|
23
|
+
),
|
|
24
|
+
).toBe("https://mainnet.helius-rpc.com/?api-key=<redacted>&foo=bar");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
test("live endpoint selection requires readiness, sticks, and rotates after later failures", async () => {
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
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
|
+
});
|
|
34
|
+
expect(await pool.select()).toBe("https://second.example/");
|
|
35
|
+
calls.length = 0;
|
|
36
|
+
expect(await pool.select()).toBe("https://second.example/");
|
|
37
|
+
expect(calls).toEqual(["https://second.example/"]);
|
|
38
|
+
unavailable.add("https://second.example/");
|
|
39
|
+
expect(await pool.check()).toBe("https://public.example/");
|
|
40
|
+
unavailable.add("https://public.example/");
|
|
41
|
+
await expect(pool.check()).rejects.toThrow("probe failed");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("pooled trade stream preserves dynamic watched tokens when it rotates and closes", async () => {
|
|
45
|
+
const originalSocket = globalThis.WebSocket;
|
|
46
|
+
let firstHealthy = true;
|
|
47
|
+
let nextId = 0;
|
|
48
|
+
const registrations: Array<{ endpoint: string; token: string }> = [];
|
|
49
|
+
const first = "https://live-primary.example/";
|
|
50
|
+
const second = "https://live-fallback.example/";
|
|
51
|
+
const mint = "So11111111111111111111111111111111111111112";
|
|
52
|
+
globalThis.WebSocket = class extends EventTarget {
|
|
53
|
+
constructor() { super(); queueMicrotask(() => this.dispatchEvent(new Event("open"))); }
|
|
54
|
+
close() {}
|
|
55
|
+
} 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);
|
|
65
|
+
let subscription: Awaited<ReturnType<typeof subscribeTrades>> | undefined;
|
|
66
|
+
try {
|
|
67
|
+
subscription = await subscribeTrades({ tokens: [], rpcUrls: [first, second], healthCheckIntervalMs: 1000, onTrade() {} });
|
|
68
|
+
await subscription.addTokens(mint);
|
|
69
|
+
firstHealthy = false;
|
|
70
|
+
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 }]);
|
|
73
|
+
expect(subscription.listTokens()).toEqual([mint]);
|
|
74
|
+
expect(subscription.hasToken(mint)).toBe(true);
|
|
75
|
+
await subscription.close();
|
|
76
|
+
await subscription.closed;
|
|
77
|
+
expect(subscription.listTokens()).toEqual([]);
|
|
78
|
+
expect(removeMock).toHaveBeenCalledTimes(2);
|
|
79
|
+
} finally {
|
|
80
|
+
await subscription?.close();
|
|
81
|
+
logsMock.mockRestore(); removeMock.mockRestore(); fetchMock.mockRestore(); globalThis.WebSocket = originalSocket;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("listenTrades refreshes migrated identities without changing the watched token list", async () => {
|
|
86
|
+
const core = await import("@solard/core");
|
|
87
|
+
const { listenTrades } = await import("./live.ts");
|
|
88
|
+
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");
|
|
92
|
+
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[] = [];
|
|
97
|
+
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;
|
|
108
|
+
});
|
|
109
|
+
let listener: Awaited<ReturnType<typeof listenTrades>> | undefined;
|
|
110
|
+
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 });
|
|
118
|
+
expect(changes).toEqual(["remove", "add"]);
|
|
119
|
+
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 }); }
|
|
20
127
|
});
|