@solard/sdk 0.2.37 → 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.
- package/dist/types/browser/client.d.ts +129 -0
- package/dist/types/browser/index.d.ts +19 -0
- package/dist/types/browser/pump.d.ts +20 -0
- package/dist/types/browser/storage.d.ts +34 -0
- package/dist/types/browser/types.d.ts +90 -0
- package/dist/types/browser/wallet.d.ts +37 -0
- package/dist/types/client.d.ts +117 -0
- package/dist/types/default.d.ts +10 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/live.d.ts +60 -0
- package/dist/types/protocol.d.ts +2 -0
- package/package.json +14 -8
- package/src/client.ts +33 -4
- package/src/export-surface.test.ts +12 -2
- package/src/index.ts +7 -1
- package/src/live-recovery.test.ts +122 -0
- package/src/live.test.ts +53 -10
- package/src/live.ts +178 -69
package/src/client.ts
CHANGED
|
@@ -43,9 +43,11 @@ import {
|
|
|
43
43
|
type TradeListener,
|
|
44
44
|
} from "./live.ts";
|
|
45
45
|
|
|
46
|
-
export type SolardOptions = {
|
|
46
|
+
export type SolardOptions = {
|
|
47
|
+
transactionVersion?: 0 | 1;
|
|
47
48
|
rpcUrl?: string;
|
|
48
49
|
rpcUrls?: readonly string[];
|
|
50
|
+
websocketUrls?: readonly string[];
|
|
49
51
|
dbPath?: string;
|
|
50
52
|
cacheTtlMs?: number;
|
|
51
53
|
};
|
|
@@ -95,12 +97,25 @@ export type SolardSellInput = {
|
|
|
95
97
|
export type SolardTradeExecutionOptions = TradeExecutionOptions;
|
|
96
98
|
|
|
97
99
|
export type SolardTradeExecutionResult = TradeResult;
|
|
98
|
-
export type Solard = {
|
|
100
|
+
export type Solard = {
|
|
101
|
+
tx:ReturnType<typeof createCoreSolard>['tx'];
|
|
102
|
+
compileGroups:ReturnType<typeof createCoreSolard>['compileGroups'];
|
|
103
|
+
simulatePlan:ReturnType<typeof createCoreSolard>['simulatePlan'];
|
|
104
|
+
sendBatchPlans:ReturnType<typeof createCoreSolard>['sendBatchPlans'];
|
|
105
|
+
prepareFastTrade: ReturnType<typeof createCoreSolard>["prepareFastTrade"];
|
|
99
106
|
listenTrades(
|
|
100
107
|
options?: Omit<ListenTradesOptions, "tokens"> & {
|
|
101
108
|
tokens?: readonly string[];
|
|
102
109
|
},
|
|
103
110
|
): Promise<TradeListener>;
|
|
111
|
+
prepareUnsignedBuy: ReturnType<typeof createCoreSolard>["prepareUnsignedBuy"];
|
|
112
|
+
prepareUnsignedTokenDeployment: ReturnType<typeof createCoreSolard>["prepareUnsignedTokenDeployment"];
|
|
113
|
+
resolvePumpLaunchQuote: ReturnType<typeof createCoreSolard>["resolvePumpLaunchQuote"];
|
|
114
|
+
simulateUnsignedTokenDeployment: ReturnType<typeof createCoreSolard>["simulateUnsignedTokenDeployment"];
|
|
115
|
+
simulateUnsignedBuy: ReturnType<typeof createCoreSolard>["simulateUnsignedBuy"];
|
|
116
|
+
prepareUnsignedSell: ReturnType<typeof createCoreSolard>["prepareUnsignedSell"];
|
|
117
|
+
simulateUnsignedSell: ReturnType<typeof createCoreSolard>["simulateUnsignedSell"];
|
|
118
|
+
proposeUnsignedBuyLookupTable: ReturnType<typeof createCoreSolard>["proposeUnsignedBuyLookupTable"];
|
|
104
119
|
resolveCurrentMarket: ReturnType<
|
|
105
120
|
typeof createCoreSolard
|
|
106
121
|
>["resolveCurrentMarket"];
|
|
@@ -268,7 +283,7 @@ async function executeTrade(
|
|
|
268
283
|
});
|
|
269
284
|
}
|
|
270
285
|
|
|
271
|
-
export function createSolard(options: SolardOptions = {}): Solard {
|
|
286
|
+
export function createSolard(options: SolardOptions = {}): Solard {
|
|
272
287
|
const core = createCoreSolard(options);
|
|
273
288
|
const history: SolardHistoryApi = Object.freeze({
|
|
274
289
|
replay: (token, replayOptions) => core.history.replay(token, replayOptions),
|
|
@@ -280,19 +295,33 @@ export function createSolard(options: SolardOptions = {}): Solard {
|
|
|
280
295
|
core.events(token, eventOptions),
|
|
281
296
|
{ merge: core.events.merge },
|
|
282
297
|
) as SolardEventsApi;
|
|
283
|
-
return Object.freeze({
|
|
298
|
+
return Object.freeze({
|
|
299
|
+
tx:core.tx.bind(core),
|
|
300
|
+
compileGroups:core.compileGroups.bind(core),
|
|
301
|
+
simulatePlan:core.simulatePlan.bind(core),
|
|
302
|
+
sendBatchPlans:core.sendBatchPlans.bind(core),
|
|
284
303
|
listenTrades: (
|
|
285
304
|
listenerOptions: Omit<ListenTradesOptions, "tokens"> & {
|
|
286
305
|
tokens?: readonly string[];
|
|
287
306
|
} = {},
|
|
288
307
|
) =>
|
|
289
308
|
openTradeListener({
|
|
309
|
+
websocketUrls: listenerOptions.websocketUrls ?? options.websocketUrls,
|
|
290
310
|
rpcUrls:
|
|
291
311
|
options.rpcUrls ?? (options.rpcUrl ? [options.rpcUrl] : undefined),
|
|
292
312
|
dbPath: options.dbPath,
|
|
293
313
|
...listenerOptions,
|
|
294
314
|
tokens: listenerOptions.tokens ?? [],
|
|
295
315
|
}),
|
|
316
|
+
prepareFastTrade: core.prepareFastTrade.bind(core),
|
|
317
|
+
prepareUnsignedBuy: core.prepareUnsignedBuy.bind(core),
|
|
318
|
+
prepareUnsignedTokenDeployment: core.prepareUnsignedTokenDeployment.bind(core),
|
|
319
|
+
resolvePumpLaunchQuote: core.resolvePumpLaunchQuote.bind(core),
|
|
320
|
+
simulateUnsignedTokenDeployment: core.simulateUnsignedTokenDeployment.bind(core),
|
|
321
|
+
simulateUnsignedBuy: core.simulateUnsignedBuy.bind(core),
|
|
322
|
+
prepareUnsignedSell: core.prepareUnsignedSell.bind(core),
|
|
323
|
+
simulateUnsignedSell: core.simulateUnsignedSell.bind(core),
|
|
324
|
+
proposeUnsignedBuyLookupTable: core.proposeUnsignedBuyLookupTable.bind(core),
|
|
296
325
|
resolveCurrentMarket: core.resolveCurrentMarket.bind(core),
|
|
297
326
|
getClaimableCreatorFees: core.getClaimableCreatorFees.bind(core),
|
|
298
327
|
claimAllCreatorFees: core.claimAllCreatorFees.bind(core),
|
|
@@ -29,7 +29,10 @@ 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: [
|
|
32
|
+
values: [
|
|
33
|
+
"composeTransactionDrafts",
|
|
34
|
+
"observeSolardRpc",
|
|
35
|
+
"withSolardRpcContext",
|
|
33
36
|
"normalizeValueBandPolicy",
|
|
34
37
|
"planValueBandDecision",
|
|
35
38
|
"valueBandThresholds",
|
|
@@ -54,7 +57,14 @@ describe("sdk membrane", () => {
|
|
|
54
57
|
"sol",
|
|
55
58
|
"tokenAmount",
|
|
56
59
|
].sort(),
|
|
57
|
-
types: [
|
|
60
|
+
types: [
|
|
61
|
+
"TransactionDraft",
|
|
62
|
+
"TransactionGroupInput",
|
|
63
|
+
"PlannedTransaction",
|
|
64
|
+
"SolardRpcObservation",
|
|
65
|
+
"SolardRpcContext",
|
|
66
|
+
"FeedRecoveryVerification",
|
|
67
|
+
"RecoverableTradeSubscription",
|
|
58
68
|
"SellEconomics",
|
|
59
69
|
"SupportedLaunchLabPair",
|
|
60
70
|
"TradeListenerMigration",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default } from "./default.ts";
|
|
2
2
|
export { createSolard } from "./client.ts";
|
|
3
|
-
export {
|
|
3
|
+
export {observeSolardRpc,withSolardRpcContext} from '@solard/core';
|
|
4
|
+
export type {SolardRpcObservation,SolardRpcContext} from '@solard/core';
|
|
5
|
+
export { createTraderSolard } from "@solard/core";
|
|
4
6
|
export {
|
|
5
7
|
normalizeValueBandPolicy,
|
|
6
8
|
planValueBandDecision,
|
|
@@ -123,6 +125,8 @@ export {
|
|
|
123
125
|
export type {
|
|
124
126
|
ListenTradesOptions,
|
|
125
127
|
LiveEndpointOptions,
|
|
128
|
+
FeedRecoveryVerification,
|
|
129
|
+
RecoverableTradeSubscription,
|
|
126
130
|
SubscribeLaunchesOptions,
|
|
127
131
|
SubscribeMigrationsOptions,
|
|
128
132
|
TradeListener,
|
|
@@ -149,3 +153,5 @@ export type {
|
|
|
149
153
|
TokenMetadataKind,
|
|
150
154
|
TokenMetadataMode,
|
|
151
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,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
const mockFetch = (implementation: (...args: Parameters<typeof fetch>) => ReturnType<typeof fetch>) =>
|
|
3
|
+
spyOn(globalThis, "fetch").mockImplementation(implementation as typeof fetch);
|
|
2
4
|
import { Connection } from "@solana/web3.js";
|
|
3
5
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
6
|
import { join } from "node:path";
|
|
@@ -26,7 +28,7 @@ describe("sdk live endpoint", () => {
|
|
|
26
28
|
redactRpcEndpoint(
|
|
27
29
|
"https://mainnet.helius-rpc.com/?api-key=secret-value&foo=bar",
|
|
28
30
|
),
|
|
29
|
-
).toBe("https://mainnet.helius-rpc.com
|
|
31
|
+
).toBe("https://mainnet.helius-rpc.com/***");
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
test("live endpoint selection requires readiness, sticks, and rotates after later failures", async () => {
|
|
@@ -57,7 +59,7 @@ test("pooled trade stream preserves dynamic watched tokens when it rotates and c
|
|
|
57
59
|
const originalSocket = globalThis.WebSocket;
|
|
58
60
|
let firstHealthy = true;
|
|
59
61
|
let nextId = 0;
|
|
60
|
-
const registrations: Array<{ endpoint: string; token: string }> = [];
|
|
62
|
+
const registrations: Array<{ endpoint: string; ws: string; token: string }> = [];
|
|
61
63
|
const first = "https://live-primary.example/";
|
|
62
64
|
const second = "https://live-fallback.example/";
|
|
63
65
|
const mint = "So11111111111111111111111111111111111111112";
|
|
@@ -66,18 +68,29 @@ test("pooled trade stream preserves dynamic watched tokens when it rotates and c
|
|
|
66
68
|
super();
|
|
67
69
|
queueMicrotask(() => this.dispatchEvent(new Event("open")));
|
|
68
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
|
+
}
|
|
69
78
|
close() {}
|
|
70
79
|
} as any;
|
|
71
|
-
const fetchMock =
|
|
72
|
-
async (input) => {
|
|
80
|
+
const fetchMock = mockFetch(
|
|
81
|
+
async (input, init) => {
|
|
73
82
|
if (String(input) === first && !firstHealthy)
|
|
74
83
|
return new Response("", { status: 401 });
|
|
75
|
-
|
|
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 } }));
|
|
76
88
|
},
|
|
77
89
|
);
|
|
78
90
|
const logsMock = spyOn(Connection.prototype, "onLogs").mockImplementation(
|
|
79
91
|
function (token) {
|
|
80
|
-
registrations.push({ endpoint: this.rpcEndpoint,
|
|
92
|
+
registrations.push({ endpoint: this.rpcEndpoint,
|
|
93
|
+
ws: (this as any)._rpcWsEndpoint, token: String(token) });
|
|
81
94
|
return ++nextId;
|
|
82
95
|
},
|
|
83
96
|
);
|
|
@@ -102,11 +115,29 @@ test("pooled trade stream preserves dynamic watched tokens when it rotates and c
|
|
|
102
115
|
)
|
|
103
116
|
await Bun.sleep(25);
|
|
104
117
|
expect(registrations).toEqual([
|
|
105
|
-
{ endpoint: first, token: mint },
|
|
106
|
-
{ endpoint: second, token: mint },
|
|
118
|
+
{ endpoint: first, ws: "wss://live-primary.example/", token: mint },
|
|
119
|
+
{ endpoint: second, ws: "wss://live-fallback.example/", token: mint },
|
|
107
120
|
]);
|
|
108
121
|
expect(subscription.listTokens()).toEqual([mint]);
|
|
109
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");
|
|
110
141
|
await subscription.close();
|
|
111
142
|
await subscription.closed;
|
|
112
143
|
expect(subscription.listTokens()).toEqual([]);
|
|
@@ -133,10 +164,22 @@ test("listenTrades refreshes migrated identities without changing the watched to
|
|
|
133
164
|
super();
|
|
134
165
|
queueMicrotask(() => this.dispatchEvent(new Event("open")));
|
|
135
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
|
+
}
|
|
136
174
|
close() {}
|
|
137
175
|
} as any;
|
|
138
|
-
const fetchMock =
|
|
139
|
-
async () =>
|
|
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
|
+
},
|
|
140
183
|
);
|
|
141
184
|
let onMigration: any;
|
|
142
185
|
let onTrade: any;
|