@solard/sdk 0.2.26 → 0.2.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solard/sdk",
3
- "version": "0.2.26",
3
+ "version": "0.2.29",
4
4
  "description": "Public Solard SDK for wallets, trading, history, live market subscriptions and token metadata.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -27,7 +27,7 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@solana/web3.js": "^1.98.4",
30
- "@solard/core": "0.2.26"
30
+ "@solard/core": "0.2.29"
31
31
  },
32
32
  "scripts": {
33
33
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.browser.json",
@@ -0,0 +1,60 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Keypair } from "@solana/web3.js";
6
+ import { Solard } from "../../core/src/core/solard.ts";
7
+ import { TransactionComposer } from "../../core/src/tx/composer.ts";
8
+ import type { PlannedTransaction, TransactionDraft } from "../../core/src/tx/types.ts";
9
+ import { createSolard } from "./client.ts";
10
+
11
+ test("SDK buy rebuild and sell preserve custom fees in immediate submitted results", async () => {
12
+ const directory = mkdtempSync(join(tmpdir(), "solard-sdk-fees-"));
13
+ const drafts: Array<TransactionDraft> = [];
14
+ // A sentinel plan is never compiled, signed, or broadcast in this test.
15
+ const payer = Keypair.generate();
16
+ const plan = { payer: payer.publicKey } as PlannedTransaction;
17
+ const build = spyOn(TransactionComposer.prototype, "build").mockImplementation(async function (this: TransactionComposer) {
18
+ drafts.push(this.snapshot());
19
+ return { ...plan, draft: this.snapshot() };
20
+ });
21
+ const signer = spyOn(Solard.prototype, "signer").mockImplementation(() => payer);
22
+ const compile = spyOn(Solard.prototype, "compile").mockImplementation(async (_payer, draft) => ({ ...plan, draft }));
23
+ let submissions = 0;
24
+ const submit = spyOn(Solard.prototype, "submitPlan").mockImplementation(async () => {
25
+ submissions += 1;
26
+ if (submissions === 1) {
27
+ throw new Error("Simulation failed: BuySlippageBelowMinBaseAmountOut");
28
+ }
29
+ return { signature: "test-signature", sender: "rpc", executionId: 42, plan };
30
+ });
31
+ const settle = spyOn(Solard.prototype, "settleSubmission").mockImplementation(async () => {
32
+ throw new Error("Immediate submission must not wait for confirmation");
33
+ });
34
+ const sdk = createSolard({ dbPath: join(directory, "test.sqlite") });
35
+ try {
36
+ const options = {
37
+ priorityFee: { cuLimit: 300_000, microLamports: 0 },
38
+ waitForConfirmation: false,
39
+ };
40
+ const buy = await sdk.buy({ token: "token", wallet: "wallet", amount: 0.1 }, options);
41
+ const sell = await sdk.sell({ token: "token", wallet: "wallet" }, options);
42
+ expect(drafts).toHaveLength(3);
43
+ for (const draft of drafts) {
44
+ expect(draft.cuLimit).toBe(300_000);
45
+ expect(draft.cuPriceMicroLamports).toBe(0);
46
+ }
47
+ expect(buy.status).toBe("submitted");
48
+ expect(sell.status).toBe("submitted");
49
+ expect(buy.executionId).toBe(42);
50
+ expect(settle).not.toHaveBeenCalled();
51
+ } finally {
52
+ sdk.close();
53
+ build.mockRestore();
54
+ signer.mockRestore();
55
+ compile.mockRestore();
56
+ submit.mockRestore();
57
+ settle.mockRestore();
58
+ rmSync(directory, { recursive: true, force: true });
59
+ }
60
+ });
package/src/client.ts CHANGED
@@ -1,148 +1,279 @@
1
- import {
2
- createTraderSolard as createCoreSolard,
3
- type ClaimCreatorRewardsOptions,
4
- type CreatorRewardClaimResult,
5
- type CumulativeDistributionExecuteOptions,
6
- type CumulativeDistributionInput,
7
- type CumulativeDistributionPlan,
8
- type CumulativeDistributionState,
9
- type HumanAmount,
10
- type MarketHistory,
11
- type MarketHistoryOptions,
12
- type MarketPrice,
13
- type MergedReplayEventStream,
14
- type ReplayEventSubscription,
15
- type ReplayHistory,
16
- type ReplayItem,
17
- type ReplayOptions,
18
- type ReplayEventsOptions,
19
- type TokenHolderSnapshot,
20
- type TokenHolderSnapshotOptions,
21
- type TokenRef,
22
- type WalletRef,
23
- type WalletPrivateKeyExport,
24
- type WalletPrivateKeyFormat,
25
- } from "@solard/core";
26
-
27
- export type SolardOptions = {
28
- rpcUrl?: string;
29
- dbPath?: string;
30
- cacheTtlMs?: number;
31
- };
32
-
33
- export type SolardHistoryApi = {
34
- replay(token: TokenRef, options?: ReplayOptions): Promise<ReplayHistory>;
35
- market(
36
- token: TokenRef,
37
- options?: MarketHistoryOptions,
38
- ): Promise<MarketHistory>;
39
- merge(
40
- histories: readonly (ReplayHistory | Iterable<ReplayItem>)[],
41
- ): ReplayItem[];
42
- };
43
-
44
- export type SolardEventsApi = {
45
- (
46
- token: TokenRef,
47
- options?: ReplayEventsOptions,
48
- ): Promise<ReplayEventSubscription>;
49
- merge(streams: readonly ReplayEventSubscription[]): MergedReplayEventStream;
50
- };
51
-
52
- export type Solard = {
53
- close(): void;
54
- history: SolardHistoryApi;
55
- events: SolardEventsApi;
56
- claims: {
57
- creatorFees: {
58
- claim(
59
- token: TokenRef,
60
- wallet: WalletRef,
61
- options?: ClaimCreatorRewardsOptions,
62
- ): Promise<CreatorRewardClaimResult>;
63
- status(
64
- id: string,
65
- ): ReturnType<
66
- ReturnType<typeof createCoreSolard>["claims"]["creatorFees"]["status"]
67
- >;
68
- };
69
- };
70
- distributions: {
71
- plan(
72
- options: CumulativeDistributionInput,
73
- ): Promise<CumulativeDistributionPlan>;
74
- execute(
75
- options: CumulativeDistributionExecuteOptions,
76
- ): Promise<CumulativeDistributionState>;
77
- status(id: string): CumulativeDistributionState | null;
78
- };
79
- createWallet: ReturnType<typeof createCoreSolard>["createWallet"];
80
- createVanityWallet: ReturnType<typeof createCoreSolard>["createVanityWallet"];
81
- importWallet: ReturnType<typeof createCoreSolard>["importWallet"];
82
- listWallets: ReturnType<typeof createCoreSolard>["listWallets"];
83
- walletAddress(wallet: WalletRef): string;
84
- exportWalletPrivateKey(
85
- wallet: WalletRef,
86
- format?: WalletPrivateKeyFormat,
87
- ): WalletPrivateKeyExport;
88
- addToken: ReturnType<typeof createCoreSolard>["addToken"];
89
- resolveToken: ReturnType<typeof createCoreSolard>["resolveToken"];
90
- tokenAccounts: ReturnType<typeof createCoreSolard>["tokenAccounts"];
91
- snapshotHolders(
92
- token: TokenRef,
93
- options?: Omit<TokenHolderSnapshotOptions, "token">,
94
- ): Promise<TokenHolderSnapshot>;
95
- walletBalances: ReturnType<typeof createCoreSolard>["walletBalances"];
96
- samplePrice(token: TokenRef): Promise<MarketPrice>;
97
- buy(
98
- token: TokenRef,
99
- wallet: WalletRef,
100
- amount: HumanAmount,
101
- options?: Parameters<ReturnType<typeof createCoreSolard>["buy"]>[3],
102
- ): ReturnType<ReturnType<typeof createCoreSolard>["buy"]>;
103
- sell: ReturnType<typeof createCoreSolard>["sell"];
104
- };
105
-
106
- export function createSolard(options: SolardOptions = {}): Solard {
107
- const core = createCoreSolard(options);
108
- const history: SolardHistoryApi = Object.freeze({
109
- replay: (token, replayOptions) => core.history.replay(token, replayOptions),
110
- market: (token, marketOptions) => core.history.market(token, marketOptions),
111
- merge: core.history.merge,
112
- });
113
- const events = Object.assign(
114
- (token: TokenRef, eventOptions: ReplayEventsOptions = {}) =>
115
- core.events(token, eventOptions),
116
- { merge: core.events.merge },
117
- ) as SolardEventsApi;
118
- return Object.freeze({
119
- close: core.close.bind(core),
120
- history,
121
- events: Object.freeze(events),
122
- claims: Object.freeze({
123
- creatorFees: Object.freeze({
124
- claim: core.claims.creatorFees.claim,
125
- status: core.claims.creatorFees.status,
126
- }),
127
- }),
128
- distributions: Object.freeze({
129
- plan: core.distributions.plan,
130
- execute: core.distributions.execute,
131
- status: core.distributions.status,
132
- }),
133
- createWallet: core.createWallet.bind(core),
134
- createVanityWallet: core.createVanityWallet.bind(core),
135
- importWallet: core.importWallet.bind(core),
136
- listWallets: core.listWallets.bind(core),
137
- walletAddress: core.walletAddress.bind(core),
138
- exportWalletPrivateKey: core.exportWalletPrivateKey.bind(core),
139
- addToken: core.addToken.bind(core),
140
- resolveToken: core.resolveToken.bind(core),
141
- tokenAccounts: core.tokenAccounts.bind(core),
142
- snapshotHolders: core.snapshotHolders.bind(core),
143
- walletBalances: core.walletBalances.bind(core),
144
- samplePrice: core.samplePrice.bind(core),
145
- buy: core.buy.bind(core),
146
- sell: core.sell.bind(core),
147
- });
148
- }
1
+ import {
2
+ createTraderSolard as createCoreSolard,
3
+ type ClaimCreatorRewardsOptions,
4
+ type CreatorRewardClaimResult,
5
+ type CumulativeDistributionExecuteOptions,
6
+ type CumulativeDistributionInput,
7
+ type CumulativeDistributionPlan,
8
+ type CumulativeDistributionState,
9
+ type HumanAmount,
10
+ type MarketHistory,
11
+ type MarketHistoryOptions,
12
+ type MarketPrice,
13
+ type MergedReplayEventStream,
14
+ type ReplayEventSubscription,
15
+ type ReplayHistory,
16
+ type ReplayItem,
17
+ type ReplayOptions,
18
+ type ReplayEventsOptions,
19
+ type RecordConfirmedTradeInput,
20
+ type SendReceipt,
21
+ type TradeLandingPolicy,
22
+ type SenderId,
23
+ type SolardDecodedTransaction,
24
+ type SolardTransactionOptions,
25
+ type SolardPosition,
26
+ type SolardPositionQuery,
27
+ type SolardTrade,
28
+ type SolardTradeQuery,
29
+ type TokenHolderSnapshot,
30
+ type TokenHolderSnapshotOptions,
31
+ type TokenRef,
32
+ type WalletRef,
33
+ type WalletPrivateKeyExport,
34
+ type WalletPrivateKeyFormat,
35
+ } from "@solard/core";
36
+
37
+ export type SolardOptions = {
38
+ rpcUrl?: string;
39
+ dbPath?: string;
40
+ cacheTtlMs?: number;
41
+ };
42
+
43
+ export type SolardHistoryApi = {
44
+ replay(token: TokenRef, options?: ReplayOptions): Promise<ReplayHistory>;
45
+ market(
46
+ token: TokenRef,
47
+ options?: MarketHistoryOptions,
48
+ ): Promise<MarketHistory>;
49
+ merge(
50
+ histories: readonly (ReplayHistory | Iterable<ReplayItem>)[],
51
+ ): ReplayItem[];
52
+ };
53
+
54
+ export type SolardEventsApi = {
55
+ (
56
+ token: TokenRef,
57
+ options?: ReplayEventsOptions,
58
+ ): Promise<ReplayEventSubscription>;
59
+ merge(streams: readonly ReplayEventSubscription[]): MergedReplayEventStream;
60
+ };
61
+
62
+ export type SolardBuyInput = {
63
+ wallet: WalletRef;
64
+ token: TokenRef;
65
+ amount: HumanAmount | string | number;
66
+ slippageBps?: number;
67
+ };
68
+
69
+ export type SolardSellInput = {
70
+ wallet: WalletRef;
71
+ token: TokenRef;
72
+ amount?: "all" | { bps: number };
73
+ slippageBps?: number;
74
+ };
75
+
76
+ export type SolardTradeExecutionOptions = {
77
+ landing?: TradeLandingPolicy;
78
+ waitForConfirmation?: boolean;
79
+ priorityFee?: { cuLimit?: number; microLamports?: number };
80
+ via?: SenderId;
81
+ skipSimulation?: boolean;
82
+ skipPreflight?: boolean;
83
+ };
84
+
85
+ export type SolardTradeExecutionResult = {
86
+ attempts?: Array<{ executionId: number; signature: string; priorityMicroLamports: number }>;
87
+ executionId: number;
88
+ signature: string;
89
+ sender: string;
90
+ status: SendReceipt["status"];
91
+ slot: number | null;
92
+ error: string | null;
93
+ feeLamports?: number;
94
+ computeUnitsConsumed?: number;
95
+ /** Signed economic SOL delta excluding network fee and token-account rent. */
96
+ solPrincipalDeltaLamports?: bigint;
97
+ /** Signed target-token delta for the confirmed atomic route. */
98
+ targetTokenDeltaRaw?: bigint;
99
+ /** Confirmed transaction network fee. */
100
+ networkFeeLamports?: bigint;
101
+ };
102
+
103
+ export type Solard = {
104
+ close(): void;
105
+ history: SolardHistoryApi;
106
+ events: SolardEventsApi;
107
+ claims: {
108
+ creatorFees: {
109
+ claim(
110
+ token: TokenRef,
111
+ wallet: WalletRef,
112
+ options?: ClaimCreatorRewardsOptions,
113
+ ): Promise<CreatorRewardClaimResult>;
114
+ status(
115
+ id: string,
116
+ ): ReturnType<
117
+ ReturnType<typeof createCoreSolard>["claims"]["creatorFees"]["status"]
118
+ >;
119
+ };
120
+ };
121
+ distributions: {
122
+ plan(
123
+ options: CumulativeDistributionInput,
124
+ ): Promise<CumulativeDistributionPlan>;
125
+ execute(
126
+ options: CumulativeDistributionExecuteOptions,
127
+ ): Promise<CumulativeDistributionState>;
128
+ status(id: string): CumulativeDistributionState | null;
129
+ };
130
+ createWallet: ReturnType<typeof createCoreSolard>["createWallet"];
131
+ createVanityWallet: ReturnType<typeof createCoreSolard>["createVanityWallet"];
132
+ importWallet: ReturnType<typeof createCoreSolard>["importWallet"];
133
+ listWallets: ReturnType<typeof createCoreSolard>["listWallets"];
134
+ walletAddress(wallet: WalletRef): string;
135
+ exportPrivateKey(
136
+ wallet: WalletRef,
137
+ format?: WalletPrivateKeyFormat,
138
+ ): WalletPrivateKeyExport;
139
+ addToken: ReturnType<typeof createCoreSolard>["addToken"];
140
+ resolveToken: ReturnType<typeof createCoreSolard>["resolveToken"];
141
+ tokenAccounts: ReturnType<typeof createCoreSolard>["tokenAccounts"];
142
+ snapshotHolders(
143
+ token: TokenRef,
144
+ options?: Omit<TokenHolderSnapshotOptions, "token">,
145
+ ): Promise<TokenHolderSnapshot>;
146
+ walletBalances: ReturnType<typeof createCoreSolard>["walletBalances"];
147
+ recordConfirmedTrade(input: RecordConfirmedTradeInput): Promise<SolardTrade>;
148
+ trades(options?: SolardTradeQuery): Promise<SolardTrade[]>;
149
+ position(options: SolardPositionQuery): Promise<SolardPosition>;
150
+ getTransaction(
151
+ signature: string,
152
+ options?: SolardTransactionOptions,
153
+ ): Promise<SolardDecodedTransaction | null>;
154
+ samplePrice(token: TokenRef): Promise<MarketPrice>;
155
+ buy(
156
+ input: SolardBuyInput,
157
+ options?: SolardTradeExecutionOptions,
158
+ ): Promise<SolardTradeExecutionResult>;
159
+ sell(
160
+ input: SolardSellInput,
161
+ options?: SolardTradeExecutionOptions,
162
+ ): Promise<SolardTradeExecutionResult>;
163
+ };
164
+
165
+ function tradeAmount(value: HumanAmount | string | number): HumanAmount {
166
+ return typeof value === "string" || typeof value === "number"
167
+ ? { sol: value }
168
+ : value;
169
+ }
170
+
171
+ function sellBps(value: SolardSellInput["amount"]): number {
172
+ if (value == null || value === "all") return 10_000;
173
+ const bps = Math.trunc(value.bps);
174
+ if (!(bps > 0 && bps <= 10_000))
175
+ throw new Error("sell amount.bps must be between 1 and 10000");
176
+ return bps;
177
+ }
178
+
179
+
180
+ async function executeTrade(
181
+ core: ReturnType<typeof createCoreSolard>,
182
+ side: "buy" | "sell",
183
+ input: SolardBuyInput | SolardSellInput,
184
+ options: SolardTradeExecutionOptions = {},
185
+ ): Promise<SolardTradeExecutionResult> {
186
+ const { submission, receipt, attempts } = await core.executeTradePlan(input.wallet, async () => {
187
+ const composer = core.tx(input.wallet).priorityFee(options.priorityFee ?? {});
188
+ if (side === "buy") {
189
+ const buy = input as SolardBuyInput;
190
+ composer.buy(buy.token, tradeAmount(buy.amount), { slippageBps: buy.slippageBps });
191
+ } else {
192
+ const sell = input as SolardSellInput;
193
+ composer.sell(sell.token, { bps: sellBps(sell.amount), slippageBps: sell.slippageBps });
194
+ }
195
+ return await composer.build();
196
+ }, options.via ?? "rpc", side, options);
197
+ const confirmedTrade =
198
+ receipt.status === "confirmed"
199
+ ? (
200
+ await core.trades({
201
+ wallet: input.wallet,
202
+ token: input.token,
203
+ side,
204
+ status: "confirmed",
205
+ limit: 32,
206
+ })
207
+ ).find((trade) => trade.signature === receipt.signature) ?? null
208
+ : null;
209
+ return {
210
+ executionId: submission.executionId,
211
+ signature: receipt.signature,
212
+ sender: receipt.sender,
213
+ status: receipt.status,
214
+ slot: receipt.slot,
215
+ error: receipt.error ?? null,
216
+ feeLamports: receipt.feeLamports,
217
+ computeUnitsConsumed: receipt.computeUnitsConsumed,
218
+ attempts,
219
+ ...(confirmedTrade?.solDeltaLamports != null
220
+ ? { solPrincipalDeltaLamports: confirmedTrade.solDeltaLamports }
221
+ : {}),
222
+ ...(confirmedTrade?.tokenDeltaRaw != null
223
+ ? { targetTokenDeltaRaw: confirmedTrade.tokenDeltaRaw }
224
+ : {}),
225
+ ...(confirmedTrade?.networkFeeLamports != null
226
+ ? { networkFeeLamports: confirmedTrade.networkFeeLamports }
227
+ : {}),
228
+ };
229
+ }
230
+
231
+ export function createSolard(options: SolardOptions = {}): Solard {
232
+ const core = createCoreSolard(options);
233
+ const history: SolardHistoryApi = Object.freeze({
234
+ replay: (token, replayOptions) => core.history.replay(token, replayOptions),
235
+ market: (token, marketOptions) => core.history.market(token, marketOptions),
236
+ merge: core.history.merge,
237
+ });
238
+ const events = Object.assign(
239
+ (token: TokenRef, eventOptions: ReplayEventsOptions = {}) =>
240
+ core.events(token, eventOptions),
241
+ { merge: core.events.merge },
242
+ ) as SolardEventsApi;
243
+ return Object.freeze({
244
+ close: core.close.bind(core),
245
+ history,
246
+ events: Object.freeze(events),
247
+ claims: Object.freeze({
248
+ creatorFees: Object.freeze({
249
+ claim: core.claims.creatorFees.claim,
250
+ status: core.claims.creatorFees.status,
251
+ }),
252
+ }),
253
+ distributions: Object.freeze({
254
+ plan: core.distributions.plan,
255
+ execute: core.distributions.execute,
256
+ status: core.distributions.status,
257
+ }),
258
+ createWallet: core.createWallet.bind(core),
259
+ createVanityWallet: core.createVanityWallet.bind(core),
260
+ importWallet: core.importWallet.bind(core),
261
+ listWallets: core.listWallets.bind(core),
262
+ walletAddress: core.walletAddress.bind(core),
263
+ exportPrivateKey: core.exportWalletPrivateKey.bind(core),
264
+ addToken: core.addToken.bind(core),
265
+ resolveToken: core.resolveToken.bind(core),
266
+ tokenAccounts: core.tokenAccounts.bind(core),
267
+ snapshotHolders: core.snapshotHolders.bind(core),
268
+ walletBalances: core.walletBalances.bind(core),
269
+ recordConfirmedTrade: core.recordConfirmedTrade.bind(core),
270
+ trades: core.trades.bind(core),
271
+ position: core.position.bind(core),
272
+ getTransaction: core.getTransaction.bind(core),
273
+ samplePrice: core.samplePrice.bind(core),
274
+ buy: (input, executionOptions) =>
275
+ executeTrade(core, "buy", input, executionOptions),
276
+ sell: (input, executionOptions) =>
277
+ executeTrade(core, "sell", input, executionOptions),
278
+ });
279
+ }
package/src/default.ts CHANGED
@@ -1,16 +1,16 @@
1
+ import { getSolUsdPrice } from "@solard/core";
1
2
  import {
2
- getSolUsdPrice,
3
+ listenTrades,
3
4
  subscribeLaunches,
4
5
  subscribeMigrations,
5
- subscribeTrades,
6
- } from "@solard/core";
6
+ } from "./live.ts";
7
7
  import { createSolard, type Solard } from "./client.ts";
8
8
 
9
9
  type DefaultSolard = Solard & {
10
10
  getSolUsdPrice: typeof getSolUsdPrice;
11
+ listenTrades: typeof listenTrades;
11
12
  subscribeLaunches: typeof subscribeLaunches;
12
13
  subscribeMigrations: typeof subscribeMigrations;
13
- subscribeTrades: typeof subscribeTrades;
14
14
  };
15
15
 
16
16
  let instance: Solard | undefined;
@@ -22,9 +22,9 @@ function client(): Solard {
22
22
 
23
23
  const operations = {
24
24
  getSolUsdPrice,
25
+ listenTrades,
25
26
  subscribeLaunches,
26
27
  subscribeMigrations,
27
- subscribeTrades,
28
28
  } as const;
29
29
 
30
30
  const slrd = new Proxy({} as DefaultSolard, {
@@ -6,6 +6,7 @@ const source = readFileSync(join(import.meta.dir, "index.ts"), "utf8");
6
6
  const client = readFileSync(join(import.meta.dir, "client.ts"), "utf8");
7
7
  const protocol = readFileSync(join(import.meta.dir, "protocol.ts"), "utf8");
8
8
  const defaultClient = readFileSync(join(import.meta.dir, "default.ts"), "utf8");
9
+ const live = readFileSync(join(import.meta.dir, "live.ts"), "utf8");
9
10
 
10
11
  function exportedNames(value: string) {
11
12
  const values: string[] = [];
@@ -30,17 +31,20 @@ describe("sdk membrane", () => {
30
31
  expect(exportedNames(source)).toEqual({
31
32
  values: [
32
33
  "createSolard",
34
+ "default",
33
35
  "fetchTokenMetadata",
34
36
  "formatRaw",
35
37
  "getSolUsdPrice",
38
+ "isDefinitivePreSubmissionError",
36
39
  "publicTokenMetadataUrl",
37
40
  "subscribeLaunches",
38
41
  "subscribeMigrations",
39
- "subscribeTrades",
42
+ "listenTrades",
40
43
  "sol",
41
44
  "tokenAmount",
42
45
  ].sort(),
43
46
  types: [
47
+ "TradeLandingPolicy",
44
48
  "ClaimCreatorRewardsOptions",
45
49
  "CreatorRewardClaimPayout",
46
50
  "CreatorRewardClaimResult",
@@ -65,6 +69,9 @@ describe("sdk membrane", () => {
65
69
  "MarketPrice",
66
70
  "SolUsdPrice",
67
71
  "SolUsdSource",
72
+ "SubscribeLaunchesOptions",
73
+ "SubscribeMigrationsOptions",
74
+ "ListenTradesOptions",
68
75
  "MergedReplayEventStream",
69
76
  "QuoteAsset",
70
77
  "ReplayCoverage",
@@ -75,15 +82,30 @@ describe("sdk membrane", () => {
75
82
  "ReplayOptions",
76
83
  "ReplayPayout",
77
84
  "ReplayTransaction",
85
+ "RecordConfirmedTradeInput",
78
86
  "SendReceipt",
79
87
  "SenderId",
80
88
  "SimulationResult",
89
+ "SolardDecodedTransaction",
90
+ "SolardPosition",
91
+ "SolardPositionQuery",
92
+ "SolardTransactionNativeBalance",
93
+ "SolardTransactionOptions",
94
+ "SolardTransactionTokenBalance",
95
+ "SolardTrade",
96
+ "SolardTradeQuery",
97
+ "SolardTradeSide",
98
+ "SolardTradeStatus",
81
99
  "TradeEvent",
82
100
  "TradeMarket",
83
101
  "TradeSide",
84
- "TradeSubscription",
102
+ "TradeListener",
85
103
  "TradeVenue",
86
104
  "Solard",
105
+ "SolardBuyInput",
106
+ "SolardSellInput",
107
+ "SolardTradeExecutionOptions",
108
+ "SolardTradeExecutionResult",
87
109
  "SolardCanonicalEvent",
88
110
  "SolardClaimAttribution",
89
111
  "SolardClaimEvent",
@@ -139,10 +161,15 @@ describe("sdk membrane", () => {
139
161
  expect(defaultClient).toContain("instance ??= createSolard()");
140
162
  expect(defaultClient).toContain('if (key === "close")');
141
163
  expect(defaultClient).toContain("getSolUsdPrice");
142
- expect(defaultClient).toContain("subscribeTrades");
164
+ expect(defaultClient).toContain("listenTrades");
143
165
  expect(defaultClient).toContain("subscribeLaunches");
144
166
  expect(defaultClient).toContain("subscribeMigrations");
145
167
  expect(defaultClient).not.toContain("export default createSolard()");
168
+ expect(live).toContain("process.env.RPC_ENDPOINT");
169
+ expect(live).not.toContain("SOLANA_RPC_URL");
170
+ expect(live).not.toContain("HELIUS_RPC_URL");
171
+ expect(live).not.toContain("SOLANA_WS_URL");
172
+ expect(live).not.toContain("HELIUS_WS_URL");
146
173
  });
147
174
 
148
175
  test("does not create persistence at module import", () => {
@@ -151,6 +178,10 @@ describe("sdk membrane", () => {
151
178
  expect(client.indexOf("createCoreSolard(options)")).toBeGreaterThan(
152
179
  client.indexOf("export function createSolard"),
153
180
  );
181
+ expect(client).toContain(
182
+ "recordConfirmedTrade: core.recordConfirmedTrade.bind(core)",
183
+ );
184
+ expect(client).toContain("trades: core.trades.bind(core)");
154
185
  });
155
186
 
156
187
  test("does not expose core repositories or secret-bearing handles", () => {
package/src/index.ts CHANGED
@@ -1,84 +1,112 @@
1
- export { default } from "./default.ts";
2
- export { createSolard } from "./client.ts";
1
+ export { default } from "./default.ts";
2
+ export { createSolard } from "./client.ts";
3
3
  export type {
4
- Solard,
5
- SolardEventsApi,
6
- SolardHistoryApi,
7
- SolardOptions,
8
- } from "./client.ts";
9
-
10
- export { formatRaw, sol, tokenAmount } from "@solard/core";
11
-
4
+ Solard,
5
+ SolardBuyInput,
6
+ SolardEventsApi,
7
+ SolardHistoryApi,
8
+ SolardOptions,
9
+ SolardSellInput,
10
+ SolardTradeExecutionOptions,
11
+ SolardTradeExecutionResult,
12
+ } from "./client.ts";
13
+
14
+ export {
15
+ formatRaw,
16
+ sol,
17
+ tokenAmount,
18
+ isDefinitivePreSubmissionError,
19
+ } from "@solard/core";
20
+
12
21
  export type {
13
22
  ClaimCreatorRewardsOptions,
14
- CreatorRewardClaimPayout,
15
- CreatorRewardClaimResult,
16
- CumulativeDistributionExecuteOptions,
17
- CumulativeDistributionInput,
18
- CumulativeDistributionPlan,
19
- CumulativeDistributionRecipient,
20
- CumulativeDistributionState,
21
- CumulativeEntitlement,
22
- HumanAmount,
23
- MarketHistory,
24
- MarketHistoryOptions,
25
- MarketPrice,
26
- MergedReplayEventStream,
27
- QuoteAsset,
28
- ReplayCoverage,
29
- ReplayEventSubscription,
30
- ReplayEventsOptions,
31
- ReplayHistory,
32
- ReplayItem,
33
- ReplayOptions,
34
- ReplayPayout,
35
- ReplayTransaction,
36
- SendReceipt,
37
- SenderId,
38
- SimulationResult,
39
- SolardCanonicalEvent,
40
- SolardClaimAttribution,
41
- SolardClaimEvent,
42
- TokenEventHistoryProgress,
43
- TokenHolder,
44
- ExcludedTokenHolder,
45
- TokenHolderSnapshot,
46
- TokenHolderSnapshotOptions,
47
- TokenRef,
48
- TokenRow,
49
- WalletInfo,
50
- WalletPrivateKeyExport,
51
- WalletPrivateKeyFormat,
52
- WalletRef,
53
- } from "@solard/core";
54
-
55
- export {
56
- fetchTokenMetadata,
57
- getSolUsdPrice,
58
- publicTokenMetadataUrl,
59
- subscribeLaunches,
60
- subscribeMigrations,
61
- subscribeTrades,
62
- } from "@solard/core";
63
- export type {
64
- LaunchEvent,
65
- LaunchSubscription,
66
- LaunchVenue,
67
- MigrationDestination,
68
- MigrationEvent,
69
- MigrationSubscription,
70
- MigrationVenue,
71
- GetSolUsdPriceOptions,
72
- SolUsdPrice,
73
- SolUsdSource,
74
- TradeEvent,
75
- TradeMarket,
76
- TradeSide,
77
- TradeSubscription,
78
- TradeVenue,
79
- FetchTokenMetadataOptions,
80
- TokenMetadata,
81
- TokenMetadataHint,
82
- TokenMetadataKind,
83
- TokenMetadataMode,
84
- } from "@solard/core";
23
+ TradeLandingPolicy,
24
+ CreatorRewardClaimPayout,
25
+ CreatorRewardClaimResult,
26
+ CumulativeDistributionExecuteOptions,
27
+ CumulativeDistributionInput,
28
+ CumulativeDistributionPlan,
29
+ CumulativeDistributionRecipient,
30
+ CumulativeDistributionState,
31
+ CumulativeEntitlement,
32
+ HumanAmount,
33
+ MarketHistory,
34
+ MarketHistoryOptions,
35
+ MarketPrice,
36
+ MergedReplayEventStream,
37
+ QuoteAsset,
38
+ ReplayCoverage,
39
+ ReplayEventSubscription,
40
+ ReplayEventsOptions,
41
+ ReplayHistory,
42
+ ReplayItem,
43
+ ReplayOptions,
44
+ ReplayPayout,
45
+ ReplayTransaction,
46
+ RecordConfirmedTradeInput,
47
+ SendReceipt,
48
+ SenderId,
49
+ SimulationResult,
50
+ SolardDecodedTransaction,
51
+ SolardPosition,
52
+ SolardPositionQuery,
53
+ SolardTransactionNativeBalance,
54
+ SolardTransactionOptions,
55
+ SolardTransactionTokenBalance,
56
+ SolardTrade,
57
+ SolardTradeQuery,
58
+ SolardTradeSide,
59
+ SolardTradeStatus,
60
+ SolardCanonicalEvent,
61
+ SolardClaimAttribution,
62
+ SolardClaimEvent,
63
+ TokenEventHistoryProgress,
64
+ TokenHolder,
65
+ ExcludedTokenHolder,
66
+ TokenHolderSnapshot,
67
+ TokenHolderSnapshotOptions,
68
+ TokenRef,
69
+ TokenRow,
70
+ WalletInfo,
71
+ WalletPrivateKeyExport,
72
+ WalletPrivateKeyFormat,
73
+ WalletRef,
74
+ } from "@solard/core";
75
+
76
+ export {
77
+ fetchTokenMetadata,
78
+ getSolUsdPrice,
79
+ publicTokenMetadataUrl,
80
+ } from "@solard/core";
81
+ export {
82
+ listenTrades,
83
+ subscribeLaunches,
84
+ subscribeMigrations,
85
+ } from "./live.ts";
86
+ export type {
87
+ ListenTradesOptions,
88
+ SubscribeLaunchesOptions,
89
+ SubscribeMigrationsOptions,
90
+ TradeListener,
91
+ } from "./live.ts";
92
+ export type {
93
+ LaunchEvent,
94
+ LaunchSubscription,
95
+ LaunchVenue,
96
+ MigrationDestination,
97
+ MigrationEvent,
98
+ MigrationSubscription,
99
+ MigrationVenue,
100
+ GetSolUsdPriceOptions,
101
+ SolUsdPrice,
102
+ SolUsdSource,
103
+ TradeEvent,
104
+ TradeMarket,
105
+ TradeSide,
106
+ TradeVenue,
107
+ FetchTokenMetadataOptions,
108
+ TokenMetadata,
109
+ TokenMetadataHint,
110
+ TokenMetadataKind,
111
+ TokenMetadataMode,
112
+ } from "@solard/core";
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { deriveWebSocketEndpoint, redactRpcEndpoint } from "./live.ts";
3
+
4
+ describe("sdk live endpoint", () => {
5
+ test("derives websocket from the one RPC endpoint", () => {
6
+ expect(
7
+ deriveWebSocketEndpoint(
8
+ "https://mainnet.helius-rpc.com/?api-key=secret-value",
9
+ ),
10
+ ).toBe("wss://mainnet.helius-rpc.com/?api-key=secret-value");
11
+ });
12
+
13
+ test("redacts API credentials in diagnostics", () => {
14
+ expect(
15
+ redactRpcEndpoint(
16
+ "https://mainnet.helius-rpc.com/?api-key=secret-value&foo=bar",
17
+ ),
18
+ ).toBe("https://mainnet.helius-rpc.com/?api-key=<redacted>&foo=bar");
19
+ });
20
+ });
package/src/live.ts ADDED
@@ -0,0 +1,332 @@
1
+ import { Connection } from "@solana/web3.js";
2
+ import {
3
+ subscribeLaunches as subscribeCoreLaunches,
4
+ subscribeMigrations as subscribeCoreMigrations,
5
+ subscribeTrades as subscribeCoreTrades,
6
+ type TradeEvent,
7
+ type TradeSubscription,
8
+ } from "@solard/core";
9
+
10
+ export type SubscribeLaunchesOptions = Omit<
11
+ Parameters<typeof subscribeCoreLaunches>[0],
12
+ "connection"
13
+ >;
14
+ export type SubscribeMigrationsOptions = Omit<
15
+ Parameters<typeof subscribeCoreMigrations>[0],
16
+ "connection"
17
+ >;
18
+ export type SubscribeTradesOptions = Omit<
19
+ Parameters<typeof subscribeCoreTrades>[0],
20
+ "connection"
21
+ >;
22
+
23
+ export type ListenTradesOptions = Omit<SubscribeTradesOptions, "onTrade">;
24
+
25
+ export type TradeListener = {
26
+ add(tokens: string | readonly string[]): Promise<void>;
27
+ remove(tokens: string | readonly string[]): Promise<void>;
28
+ has(token: string): boolean;
29
+ list(): string[];
30
+ onTrade(callback: (event: TradeEvent) => void | Promise<void>): () => void;
31
+ close(): Promise<void>;
32
+ readonly closed: Promise<void>;
33
+ };
34
+
35
+ type LiveConnection = {
36
+ endpoint: string;
37
+ websocketEndpoint: string;
38
+ connection: Connection;
39
+ };
40
+
41
+ let shared: LiveConnection | null = null;
42
+ let readiness: { endpoint: string; promise: Promise<void> } | null = null;
43
+
44
+ function endpointFromEnv(): string {
45
+ const endpoint = process.env.RPC_ENDPOINT?.trim();
46
+ if (!endpoint)
47
+ throw new Error(
48
+ "Missing RPC_ENDPOINT. Set one Solana RPC URL in RPC_ENDPOINT; Solard derives WebSocket access from the same endpoint.",
49
+ );
50
+ let parsed: URL;
51
+ try {
52
+ parsed = new URL(endpoint);
53
+ } catch {
54
+ throw new Error("RPC_ENDPOINT must be a valid http:// or https:// URL.");
55
+ }
56
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
57
+ throw new Error("RPC_ENDPOINT must use http:// or https://.");
58
+ return endpoint;
59
+ }
60
+
61
+ export function deriveWebSocketEndpoint(endpoint: string): string {
62
+ const url = new URL(endpoint);
63
+ if (url.protocol === "https:") url.protocol = "wss:";
64
+ else if (url.protocol === "http:") url.protocol = "ws:";
65
+ else throw new Error("RPC endpoint must use http:// or https://.");
66
+ return url.toString();
67
+ }
68
+
69
+ export function redactRpcEndpoint(endpoint: string): string {
70
+ try {
71
+ const url = new URL(endpoint);
72
+ const keys: string[] = [];
73
+ url.searchParams.forEach((_value, key) => keys.push(key));
74
+ for (const key of keys) {
75
+ if (/key|token|secret|auth/i.test(key))
76
+ url.searchParams.set(key, "<redacted>");
77
+ }
78
+ return url.toString().replaceAll("%3Credacted%3E", "<redacted>");
79
+ } catch {
80
+ return endpoint.replace(
81
+ /([?&](?:api-?key|api_key|apikey|key|token|secret|auth)=)[^&]+/gi,
82
+ "$1<redacted>",
83
+ );
84
+ }
85
+ }
86
+
87
+ function providerName(endpoint: string): string {
88
+ try {
89
+ return new URL(endpoint).hostname.endsWith("helius-rpc.com")
90
+ ? "Helius"
91
+ : "Solana RPC provider";
92
+ } catch {
93
+ return "Solana RPC provider";
94
+ }
95
+ }
96
+
97
+ function safeMessage(error: unknown): string {
98
+ const value = error instanceof Error ? error.message : String(error);
99
+ return value
100
+ .replace(
101
+ /([?&](?:api-?key|api_key|apikey|key|token|secret|auth)=)[^&\s]+/gi,
102
+ "$1<redacted>",
103
+ )
104
+ .slice(0, 400);
105
+ }
106
+
107
+ function rpcFailure(endpoint: string, detail: string): Error {
108
+ return new Error(
109
+ `${providerName(endpoint)} rejected RPC_ENDPOINT ${redactRpcEndpoint(endpoint)}. Check the API key, credits/quota, rate limits, and account status. ${detail}`,
110
+ );
111
+ }
112
+
113
+ function websocketFailure(endpoint: string, detail: string): Error {
114
+ return new Error(
115
+ `${providerName(endpoint)} rejected WebSocket access derived from RPC_ENDPOINT ${redactRpcEndpoint(endpoint)}. Check the API key, credits/quota, rate limits, and WebSocket connection limits. ${detail}`,
116
+ );
117
+ }
118
+
119
+ async function preflightRpc(endpoint: string): Promise<void> {
120
+ const controller = new AbortController();
121
+ const timer = setTimeout(() => controller.abort(), 6_000);
122
+ try {
123
+ const response = await fetch(endpoint, {
124
+ method: "POST",
125
+ headers: { "content-type": "application/json" },
126
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getHealth" }),
127
+ signal: controller.signal,
128
+ });
129
+ if (!response.ok) {
130
+ const label =
131
+ response.status === 401 || response.status === 403
132
+ ? "Authentication failed."
133
+ : response.status === 402
134
+ ? "Billing or credits are unavailable."
135
+ : response.status === 429
136
+ ? "Rate limit or quota was exceeded."
137
+ : `HTTP ${response.status}.`;
138
+ throw rpcFailure(endpoint, label);
139
+ }
140
+ const body = (await response.json()) as {
141
+ result?: unknown;
142
+ error?: { code?: unknown; message?: unknown };
143
+ };
144
+ if (body.error) {
145
+ const code =
146
+ body.error.code == null ? "" : `code ${String(body.error.code)}: `;
147
+ throw rpcFailure(
148
+ endpoint,
149
+ `${code}${String(body.error.message ?? "RPC error")}`,
150
+ );
151
+ }
152
+ } catch (error) {
153
+ if (
154
+ error instanceof Error &&
155
+ error.message.includes("rejected RPC_ENDPOINT")
156
+ )
157
+ throw error;
158
+ if (controller.signal.aborted)
159
+ throw rpcFailure(endpoint, "Connection timed out.");
160
+ throw rpcFailure(endpoint, safeMessage(error));
161
+ } finally {
162
+ clearTimeout(timer);
163
+ }
164
+ }
165
+
166
+ async function preflightWebSocket(endpoint: string): Promise<void> {
167
+ const websocketEndpoint = deriveWebSocketEndpoint(endpoint);
168
+ await new Promise<void>((resolve, reject) => {
169
+ let settled = false;
170
+ let socket: WebSocket | null = null;
171
+ const finish = (error?: Error) => {
172
+ if (settled) return;
173
+ settled = true;
174
+ clearTimeout(timer);
175
+ if (error && socket) {
176
+ try {
177
+ socket.close();
178
+ } catch {}
179
+ }
180
+ if (error) reject(error);
181
+ else resolve();
182
+ };
183
+ const timer = setTimeout(
184
+ () =>
185
+ finish(websocketFailure(endpoint, "WebSocket handshake timed out.")),
186
+ 6_000,
187
+ );
188
+ try {
189
+ socket = new WebSocket(websocketEndpoint);
190
+ } catch (error) {
191
+ finish(websocketFailure(endpoint, safeMessage(error)));
192
+ return;
193
+ }
194
+ socket.addEventListener(
195
+ "open",
196
+ () => {
197
+ finish();
198
+ try {
199
+ socket?.close(1000);
200
+ } catch {}
201
+ },
202
+ { once: true },
203
+ );
204
+ socket.addEventListener(
205
+ "error",
206
+ (event) => {
207
+ const detail = safeMessage(
208
+ (event as { error?: unknown }).error ??
209
+ "WebSocket handshake was rejected.",
210
+ );
211
+ finish(websocketFailure(endpoint, detail));
212
+ },
213
+ { once: true },
214
+ );
215
+ socket.addEventListener(
216
+ "close",
217
+ (event) => {
218
+ if (settled) return;
219
+ const detail = event.reason
220
+ ? `WebSocket closed during handshake: ${event.reason}`
221
+ : `WebSocket closed during handshake with code ${event.code}.`;
222
+ finish(websocketFailure(endpoint, detail));
223
+ },
224
+ { once: true },
225
+ );
226
+ });
227
+ }
228
+
229
+ async function ensureReady(endpoint: string): Promise<void> {
230
+ if (readiness?.endpoint === endpoint) return await readiness.promise;
231
+ const promise = Promise.all([
232
+ preflightRpc(endpoint),
233
+ preflightWebSocket(endpoint),
234
+ ]).then(() => undefined);
235
+ readiness = { endpoint, promise };
236
+ try {
237
+ await promise;
238
+ } catch (error) {
239
+ if (readiness?.promise === promise) readiness = null;
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ async function liveConnection(): Promise<LiveConnection> {
245
+ const endpoint = endpointFromEnv();
246
+ await ensureReady(endpoint);
247
+ if (shared?.endpoint === endpoint) return shared;
248
+ shared = {
249
+ endpoint,
250
+ websocketEndpoint: deriveWebSocketEndpoint(endpoint),
251
+ connection: new Connection(endpoint, "confirmed"),
252
+ };
253
+ return shared;
254
+ }
255
+
256
+ function notifyReady(
257
+ options: {
258
+ onStatus?: (event: string, data?: Record<string, unknown>) => void;
259
+ },
260
+ live: LiveConnection,
261
+ ): void {
262
+ try {
263
+ options.onStatus?.("rpc-ready", {
264
+ rpc: redactRpcEndpoint(live.endpoint),
265
+ websocket: redactRpcEndpoint(live.websocketEndpoint),
266
+ });
267
+ } catch {}
268
+ }
269
+
270
+ export async function subscribeTrades(options: SubscribeTradesOptions) {
271
+ const live = await liveConnection();
272
+ notifyReady(options, live);
273
+ return await subscribeCoreTrades({ ...options, connection: live.connection });
274
+ }
275
+
276
+ export async function subscribeLaunches(options: SubscribeLaunchesOptions) {
277
+ const live = await liveConnection();
278
+ notifyReady(options, live);
279
+ return await subscribeCoreLaunches({
280
+ ...options,
281
+ connection: live.connection,
282
+ });
283
+ }
284
+
285
+ export async function subscribeMigrations(options: SubscribeMigrationsOptions) {
286
+ const live = await liveConnection();
287
+ notifyReady(options, live);
288
+ return await subscribeCoreMigrations({
289
+ ...options,
290
+ connection: live.connection,
291
+ });
292
+ }
293
+ export async function listenTrades(
294
+ options: ListenTradesOptions,
295
+ ): Promise<TradeListener> {
296
+ const callbacks = new Set<(event: TradeEvent) => void | Promise<void>>();
297
+ const pending: TradeEvent[] = [];
298
+ let subscription: TradeSubscription;
299
+ const dispatch = async (event: TradeEvent) => {
300
+ if (callbacks.size === 0) {
301
+ pending.push(event);
302
+ if (pending.length > 1_000) pending.shift();
303
+ return;
304
+ }
305
+ await Promise.all(
306
+ [...callbacks].map(async (callback) => {
307
+ await callback(event);
308
+ }),
309
+ );
310
+ };
311
+ subscription = await subscribeTrades({ ...options, onTrade: dispatch });
312
+ return {
313
+ add: (tokens) => subscription.addTokens(tokens),
314
+ remove: (tokens) => subscription.removeTokens(tokens),
315
+ has: (token) => subscription.hasToken(token),
316
+ list: () => subscription.listTokens(),
317
+ onTrade(callback) {
318
+ callbacks.add(callback);
319
+ if (pending.length) {
320
+ const rows = pending.splice(0);
321
+ void Promise.resolve().then(async () => {
322
+ for (const event of rows) await callback(event);
323
+ });
324
+ }
325
+ return () => callbacks.delete(callback);
326
+ },
327
+ close: () => subscription.close(),
328
+ get closed() {
329
+ return subscription.closed;
330
+ },
331
+ };
332
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ const client = readFileSync(join(import.meta.dir, "client.ts"), "utf8");
6
+ const live = readFileSync(join(import.meta.dir, "live.ts"), "utf8");
7
+ const index = readFileSync(join(import.meta.dir, "index.ts"), "utf8");
8
+ const core = readFileSync(
9
+ join(import.meta.dir, "../../core/src/core/solard.ts"),
10
+ "utf8",
11
+ );
12
+
13
+ describe("operator api", () => {
14
+ test("uses one object-form trade api", () => {
15
+ expect(client).toContain("input: SolardBuyInput");
16
+ expect(client).toContain("input: SolardSellInput");
17
+ expect(client).toContain("waitForConfirmation?: boolean");
18
+ expect(client).toContain("exportPrivateKey");
19
+ expect(client).toContain("getTransaction(");
20
+ });
21
+
22
+ test("uses one dynamic live trade listener api", () => {
23
+ expect(index).toContain("listenTrades");
24
+ expect(index).not.toContain("subscribeTrades");
25
+ expect(live).toContain("onTrade(callback)");
26
+ expect(live).toContain("add: (tokens)");
27
+ expect(live).toContain("remove: (tokens)");
28
+ });
29
+
30
+ test("persists the signed signature before safe rebroadcast", () => {
31
+ expect(core).toContain("const signature = signedPlanSignature(plan)");
32
+ expect(core).toContain(
33
+ "waitForSignatureSeen(this.connection(), signature)",
34
+ );
35
+ expect(core).toContain("await broadcast(true)");
36
+ expect(core).toContain("cacheParsedTransaction");
37
+ });
38
+
39
+ test("default trades settle inside Solard rather than leaking submitted state", () => {
40
+ expect(client).toContain("core.executeTradePlan(");
41
+ expect(core).toContain("async settleSubmission(");
42
+ expect(core).toContain("submission.plan.lastValidBlockHeight");
43
+ });
44
+ });