@solard/cli 0.2.27 → 0.2.31

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.
@@ -1,239 +1,248 @@
1
- import {
2
- executeJupiterSwap,
3
- NATIVE_SOL_MINT,
4
- nativePumpTradeAvailable,
5
- quoteJupiterSwap,
6
- resolveTradeRoute,
7
- type SenderId,
8
- type Solard,
9
- } from "@solard/core";
10
-
11
- import { resolveTradeTargets, type TradeTargets } from "./trade-targets.ts";
12
- import { summarizeTradeRoute, type TradeRouteSummary } from "./trade-result.ts";
13
- import { parseVenuePreference, type TradeCommandFlags } from "./trade-venue.ts";
14
-
15
- export type JupiterSellWalletPlan = {
16
- wallet: string;
17
- balanceRaw: bigint;
18
- amountRaw: bigint;
19
- };
20
-
21
- export type SmartSellCommandResult =
22
- | {
23
- mode: "simulation";
24
- route: "native";
25
- target: TradeTargets;
26
- routing: TradeRouteSummary;
27
- results: unknown[];
28
- }
29
- | {
30
- mode: "quote";
31
- route: "jupiter";
32
- target: TradeTargets;
33
- routing: TradeRouteSummary;
34
- plans: Array<
35
- JupiterSellWalletPlan & {
36
- quote: Awaited<ReturnType<typeof quoteJupiterSwap>>;
37
- }
38
- >;
39
- hint: string;
40
- }
41
- | {
42
- mode: "live";
43
- route: "native";
44
- target: TradeTargets;
45
- routing: TradeRouteSummary;
46
- receipts: unknown;
47
- }
48
- | {
49
- mode: "live";
50
- route: "jupiter";
51
- target: TradeTargets;
52
- routing: TradeRouteSummary;
53
- results: Array<
54
- JupiterSellWalletPlan & {
55
- result: Awaited<ReturnType<typeof executeJupiterSwap>>;
56
- }
57
- >;
58
- };
59
-
60
- type RunSmartSellCommandArgs = {
61
- slrd: Solard;
62
- values: string[];
63
- flags: TradeCommandFlags;
64
- };
65
-
66
- function intFlag(
67
- flags: TradeCommandFlags,
68
- key: string,
69
- fallback: number,
70
- ): number {
71
- const value = flags.get(key);
72
- if (!value || value === "true") return fallback;
73
- const parsed = Number(value);
74
- if (!Number.isInteger(parsed)) throw new Error(`--${key} must be an integer`);
75
- return parsed;
76
- }
77
-
78
- export async function jupiterSellableAtaBalance(
79
- slrd: Solard,
80
- walletRef: string,
81
- mint: string,
82
- ): Promise<bigint> {
83
- const accounts = await slrd.tokenAccounts(walletRef);
84
- let associatedRaw = 0n;
85
-
86
- for (const account of accounts) {
87
- if (
88
- account.mint === mint &&
89
- account.isAssociated &&
90
- account.amountRaw > 0n
91
- ) {
92
- associatedRaw += account.amountRaw;
93
- }
94
- }
95
-
96
- return associatedRaw;
97
- }
98
-
99
- async function jupiterSellPlan(
100
- slrd: Solard,
101
- wallet: string,
102
- mint: string,
103
- bps: number,
104
- ): Promise<JupiterSellWalletPlan> {
105
- const balanceRaw = await jupiterSellableAtaBalance(slrd, wallet, mint);
106
- const amountRaw = (balanceRaw * BigInt(bps)) / 10_000n;
107
- if (amountRaw <= 0n) {
108
- throw new Error(`No sellable associated-token-account balance for ${mint}`);
109
- }
110
- return { wallet, balanceRaw, amountRaw };
111
- }
112
-
113
- export async function runSmartSellCommand({
114
- slrd,
115
- values,
116
- flags,
117
- }: RunSmartSellCommandArgs): Promise<SmartSellCommandResult> {
118
- const tokenRef = values[0];
119
- const usage =
120
- "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) [--bps 10000] [--venue auto|native|jupiter]";
121
- if (!tokenRef) throw new Error(usage);
122
-
123
- const bps = intFlag(flags, "bps", 10_000);
124
- if (bps <= 0 || bps > 10_000) throw new Error("--bps must be 1..10000");
125
-
126
- const target = resolveTradeTargets(slrd, flags, usage);
127
- const resolution = await resolveTradeRoute(
128
- slrd,
129
- tokenRef,
130
- parseVenuePreference(flags),
131
- );
132
-
133
- if (resolution.asset.mint === NATIVE_SOL_MINT) {
134
- throw new Error("slrd sell expects an SPL token");
135
- }
136
-
137
- if (resolution.route === "native") {
138
- if (!nativePumpTradeAvailable(resolution.token)) {
139
- throw new Error(
140
- "Native sell requires a registered Pump/PumpSwap token; use --venue jupiter for ordinary SPL assets",
141
- );
142
- }
143
-
144
- const options = {
145
- bps,
146
- slippageBps: intFlag(flags, "slippage-bps", 1_500),
147
- via: (flags.get("sender") ?? "rpc") as SenderId,
148
- skipSimulation: flags.has("skip-simulation"),
149
- skipPreflight:
150
- flags.has("skip-preflight") || flags.has("skip-simulation"),
151
- };
152
-
153
- if (flags.has("simulate-only")) {
154
- const plans =
155
- target.refs.length === 1
156
- ? [
157
- await slrd
158
- .tx(target.refs[0]!)
159
- .sell(resolution.token!.mint, options)
160
- .build(),
161
- ]
162
- : await slrd
163
- .composeMany(target.refs)
164
- .sell(resolution.token!.mint, options)
165
- .build();
166
- const results = await Promise.all(
167
- plans.map((plan) => slrd.simulatePlan(plan)),
168
- );
169
- return {
170
- mode: "simulation",
171
- route: "native",
172
- target,
173
- routing: summarizeTradeRoute(resolution),
174
- results,
175
- };
176
- }
177
-
178
- const receipts =
179
- target.refs.length === 1
180
- ? await slrd.sell(resolution.token!.mint, target.refs[0]!, options)
181
- : await slrd.sellMany(resolution.token!.mint, target.refs, options);
182
-
183
- return {
184
- mode: "live",
185
- route: "native",
186
- target,
187
- routing: summarizeTradeRoute(resolution),
188
- receipts,
189
- };
190
- }
191
-
192
- const plans: JupiterSellWalletPlan[] = [];
193
- for (const wallet of target.refs) {
194
- plans.push(await jupiterSellPlan(slrd, wallet, resolution.asset.mint, bps));
195
- }
196
-
197
- if (flags.has("simulate-only")) {
198
- const quoted = [];
199
- for (const plan of plans) {
200
- quoted.push({
201
- ...plan,
202
- quote: await quoteJupiterSwap({
203
- inputMint: resolution.asset.mint,
204
- outputMint: NATIVE_SOL_MINT,
205
- amountRaw: plan.amountRaw,
206
- }),
207
- });
208
- }
209
- return {
210
- mode: "quote",
211
- route: "jupiter",
212
- target,
213
- routing: summarizeTradeRoute(resolution),
214
- plans: quoted,
215
- hint: "Remove --simulate-only to execute these Jupiter-routed sells.",
216
- };
217
- }
218
-
219
- const results = [];
220
- for (const plan of plans) {
221
- results.push({
222
- ...plan,
223
- result: await executeJupiterSwap({
224
- inputMint: resolution.asset.mint,
225
- outputMint: NATIVE_SOL_MINT,
226
- amountRaw: plan.amountRaw,
227
- signer: slrd.signer(plan.wallet),
228
- }),
229
- });
230
- }
231
-
232
- return {
233
- mode: "live",
234
- route: "jupiter",
235
- target,
236
- routing: summarizeTradeRoute(resolution),
237
- results,
238
- };
239
- }
1
+ import {
2
+ executeJupiterSwap,
3
+ NATIVE_SOL_MINT,
4
+ nativePumpTradeAvailable,
5
+ quoteJupiterSwap,
6
+ resolveTradeRoute,
7
+ type SenderId,
8
+ type Solard,
9
+ } from "@solard/core";
10
+
11
+ import { resolveTradeTargets, type TradeTargets } from "./trade-targets.ts";
12
+ import { tradeFeeOptions } from "./trade-fees.ts";
13
+ import { summarizeTradeRoute, type TradeRouteSummary } from "./trade-result.ts";
14
+ import { parseVenuePreference, type TradeCommandFlags } from "./trade-venue.ts";
15
+
16
+ export type JupiterSellWalletPlan = {
17
+ wallet: string;
18
+ balanceRaw: bigint;
19
+ amountRaw: bigint;
20
+ };
21
+
22
+ export type SmartSellCommandResult =
23
+ | {
24
+ mode: "simulation";
25
+ route: "native";
26
+ target: TradeTargets;
27
+ routing: TradeRouteSummary;
28
+ results: unknown[];
29
+ }
30
+ | {
31
+ mode: "quote";
32
+ route: "jupiter";
33
+ target: TradeTargets;
34
+ routing: TradeRouteSummary;
35
+ plans: Array<
36
+ JupiterSellWalletPlan & {
37
+ quote: Awaited<ReturnType<typeof quoteJupiterSwap>>;
38
+ }
39
+ >;
40
+ hint: string;
41
+ }
42
+ | {
43
+ mode: "live";
44
+ route: "native";
45
+ target: TradeTargets;
46
+ routing: TradeRouteSummary;
47
+ receipts: unknown;
48
+ }
49
+ | {
50
+ mode: "live";
51
+ route: "jupiter";
52
+ target: TradeTargets;
53
+ routing: TradeRouteSummary;
54
+ results: Array<
55
+ JupiterSellWalletPlan & {
56
+ result: Awaited<ReturnType<typeof executeJupiterSwap>>;
57
+ }
58
+ >;
59
+ };
60
+
61
+ type RunSmartSellCommandArgs = {
62
+ slrd: Solard;
63
+ values: string[];
64
+ flags: TradeCommandFlags;
65
+ };
66
+
67
+ function intFlag(
68
+ flags: TradeCommandFlags,
69
+ key: string,
70
+ fallback: number,
71
+ ): number {
72
+ const value = flags.get(key);
73
+ if (!value || value === "true") return fallback;
74
+ const parsed = Number(value);
75
+ if (!Number.isInteger(parsed)) throw new Error(`--${key} must be an integer`);
76
+ return parsed;
77
+ }
78
+
79
+ export async function jupiterSellableAtaBalance(
80
+ slrd: Solard,
81
+ walletRef: string,
82
+ mint: string,
83
+ ): Promise<bigint> {
84
+ const accounts = await slrd.tokenAccounts(walletRef);
85
+ let associatedRaw = 0n;
86
+
87
+ for (const account of accounts) {
88
+ if (
89
+ account.mint === mint &&
90
+ account.isAssociated &&
91
+ account.amountRaw > 0n
92
+ ) {
93
+ associatedRaw += account.amountRaw;
94
+ }
95
+ }
96
+
97
+ return associatedRaw;
98
+ }
99
+
100
+ async function jupiterSellPlan(
101
+ slrd: Solard,
102
+ wallet: string,
103
+ mint: string,
104
+ bps: number,
105
+ ): Promise<JupiterSellWalletPlan> {
106
+ const balanceRaw = await jupiterSellableAtaBalance(slrd, wallet, mint);
107
+ const amountRaw = (balanceRaw * BigInt(bps)) / 10_000n;
108
+ if (amountRaw <= 0n) {
109
+ throw new Error(`No sellable associated-token-account balance for ${mint}`);
110
+ }
111
+ return { wallet, balanceRaw, amountRaw };
112
+ }
113
+
114
+ export async function runSmartSellCommand({
115
+ slrd,
116
+ values,
117
+ flags,
118
+ }: RunSmartSellCommandArgs): Promise<SmartSellCommandResult> {
119
+ const tokenRef = values[0];
120
+ const usage =
121
+ "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) [--bps 10000] [--venue auto|native|jupiter]";
122
+ if (!tokenRef) throw new Error(usage);
123
+
124
+ const bps = intFlag(flags, "bps", 10_000);
125
+ if (bps <= 0 || bps > 10_000) throw new Error("--bps must be 1..10000");
126
+
127
+ const target = resolveTradeTargets(slrd, flags, usage);
128
+ const resolution = await resolveTradeRoute(
129
+ slrd,
130
+ tokenRef,
131
+ parseVenuePreference(flags),
132
+ );
133
+
134
+ if (resolution.asset.mint === NATIVE_SOL_MINT) {
135
+ throw new Error("slrd sell expects an SPL token");
136
+ }
137
+
138
+ if (resolution.route === "native") {
139
+ if (!nativePumpTradeAvailable(resolution.token)) {
140
+ throw new Error(
141
+ "Native sell requires a registered Pump/PumpSwap token; use --venue jupiter for ordinary SPL assets",
142
+ );
143
+ }
144
+
145
+ const options = {
146
+ ...tradeFeeOptions(flags),
147
+ bps,
148
+ slippageBps: intFlag(flags, "slippage-bps", 1_500),
149
+ via: (flags.get("sender") ?? "rpc") as SenderId,
150
+ skipSimulation: flags.has("skip-simulation"),
151
+ skipPreflight:
152
+ flags.has("skip-preflight") || flags.has("skip-simulation"),
153
+ };
154
+
155
+ if (flags.has("simulate-only")) {
156
+ const plans =
157
+ target.refs.length === 1
158
+ ? [
159
+ await slrd
160
+ .tx(target.refs[0]!)
161
+ .priorityFee(options.priorityFee)
162
+ .sell(resolution.token!.mint, options)
163
+ .build(),
164
+ ]
165
+ : await slrd
166
+ .composeMany(target.refs)
167
+ .priorityFee(options.priorityFee)
168
+ .sell(resolution.token!.mint, options)
169
+ .build();
170
+ const results = await Promise.all(
171
+ plans.map(async (plan, index) => {
172
+ const prepared = await slrd.prepareTradePlan(target.refs[index]!, plan, options);
173
+ return { ...await slrd.simulatePlan(prepared.plan),
174
+ priorityMicroLamports: prepared.priorityMicroLamports,
175
+ cuLimit: prepared.plan.draft.cuLimit ?? 600_000 };
176
+ }),
177
+ );
178
+ return {
179
+ mode: "simulation",
180
+ route: "native",
181
+ target,
182
+ routing: summarizeTradeRoute(resolution),
183
+ results,
184
+ };
185
+ }
186
+
187
+ const receipts =
188
+ target.refs.length === 1
189
+ ? await slrd.sell(resolution.token!.mint, target.refs[0]!, options)
190
+ : await slrd.sellMany(resolution.token!.mint, target.refs, options);
191
+
192
+ return {
193
+ mode: "live",
194
+ route: "native",
195
+ target,
196
+ routing: summarizeTradeRoute(resolution),
197
+ receipts,
198
+ };
199
+ }
200
+
201
+ const plans: JupiterSellWalletPlan[] = [];
202
+ for (const wallet of target.refs) {
203
+ plans.push(await jupiterSellPlan(slrd, wallet, resolution.asset.mint, bps));
204
+ }
205
+
206
+ if (flags.has("simulate-only")) {
207
+ const quoted = [];
208
+ for (const plan of plans) {
209
+ quoted.push({
210
+ ...plan,
211
+ quote: await quoteJupiterSwap({
212
+ inputMint: resolution.asset.mint,
213
+ outputMint: NATIVE_SOL_MINT,
214
+ amountRaw: plan.amountRaw,
215
+ }),
216
+ });
217
+ }
218
+ return {
219
+ mode: "quote",
220
+ route: "jupiter",
221
+ target,
222
+ routing: summarizeTradeRoute(resolution),
223
+ plans: quoted,
224
+ hint: "Remove --simulate-only to execute these Jupiter-routed sells.",
225
+ };
226
+ }
227
+
228
+ const results = [];
229
+ for (const plan of plans) {
230
+ results.push({
231
+ ...plan,
232
+ result: await executeJupiterSwap({
233
+ inputMint: resolution.asset.mint,
234
+ outputMint: NATIVE_SOL_MINT,
235
+ amountRaw: plan.amountRaw,
236
+ signer: slrd.signer(plan.wallet),
237
+ }),
238
+ });
239
+ }
240
+
241
+ return {
242
+ mode: "live",
243
+ route: "jupiter",
244
+ target,
245
+ routing: summarizeTradeRoute(resolution),
246
+ results,
247
+ };
248
+ }
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "bun:test";
2
+ import { tradeFeeOptions } from "./trade-fees.ts";
3
+
4
+ test("native buy/sell fee flags preserve custom compute limit and price", () => {
5
+ expect(tradeFeeOptions(new Map([
6
+ ["cu-limit", "150000"], ["priority-micro-lamports", "1000000"],
7
+ ["trade-attempts", "4"], ["max-priority-fee-lamports", "500000"],
8
+ ]))).toMatchObject({ priorityFee: { cuLimit: 150_000, microLamports: 1_000_000 },
9
+ landing: { maxAttempts: 4, maxPriorityFeeLamports: 500_000 } });
10
+ });
11
+
12
+ test("invalid fee flags reject instead of being ignored", () => {
13
+ for (const [name, value] of [["cu-limit", "0"], ["priority-micro-lamports", "-1"], ["priority-micro-lamports", "true"]])
14
+ expect(() => tradeFeeOptions(new Map([[name!, value!]]))).toThrow();
15
+ });
@@ -0,0 +1,24 @@
1
+ import type { TradeLandingPolicy } from "@solard/core";
2
+
3
+ /** Validate fee flags before constructing or signing a trade. */
4
+ export function tradeFeeOptions(flags: ReadonlyMap<string, string>) {
5
+ const number = (name: string): number | undefined => {
6
+ if (!flags.has(name)) return undefined;
7
+ const value = Number(flags.get(name));
8
+ if (!Number.isFinite(value)) throw new Error(`--${name} must be a number`);
9
+ return value;
10
+ };
11
+ const cuLimit = number("cu-limit");
12
+ const microLamports = number("priority-micro-lamports");
13
+ if (cuLimit != null && (!Number.isInteger(cuLimit) || cuLimit < 1 || cuLimit > 1_400_000))
14
+ throw new Error("--cu-limit must be an integer between 1 and 1400000");
15
+ if (microLamports != null && (!Number.isSafeInteger(microLamports) || microLamports < 0))
16
+ throw new Error("--priority-micro-lamports must be a non-negative integer");
17
+ const landing: TradeLandingPolicy = {
18
+ maxAttempts: number("trade-attempts"),
19
+ feeMultiplier: number("fee-multiplier"),
20
+ feePercentile: number("fee-percentile"),
21
+ maxPriorityFeeLamports: number("max-priority-fee-lamports"),
22
+ };
23
+ return { priorityFee: { cuLimit, microLamports }, landing };
24
+ }
@@ -0,0 +1,33 @@
1
+ import { test, expect } from "bun:test";
2
+ import { Keypair } from "@solana/web3.js";
3
+ import { Solard, estimatePlanFee, type PlannedTransaction } from "@solard/core";
4
+ import { runTransferCommand } from "./transfer.ts";
5
+ function fixture(status: "confirmed" | "submitted" | "failed" = "confirmed") {
6
+ const core: Solard = Object.create(Solard.prototype); const payer = Keypair.generate();
7
+ const prices: number[] = []; let samples = 0;
8
+ core.signer = () => payer;
9
+ core.connection = () => ({
10
+ getRecentPrioritizationFees: async () => { samples++; return [200_000, 400_000, 800_000, 1_000_000].map((prioritizationFee) => ({ prioritizationFee })); },
11
+ getFeeForMessage: async (message: { price: number }) => ({ value: 5000 + Math.ceil(10_000 * message.price / 1_000_000) }),
12
+ }) as any;
13
+ core.compile = async (_payer, draft) => ({ draft, payer: payer.publicKey, transaction: { message: { price: draft.cuPriceMicroLamports ?? 100_000 } } }) as unknown as PlannedTransaction;
14
+ core.submitPlan = async (plan, sender) => { prices.push(plan.draft.cuPriceMicroLamports!); return { signature: "test-signature", executionId: 1, sender, plan, feeEstimate: await estimatePlanFee(core.connection(), plan) }; };
15
+ core.settleSubmission = async () => ({ signature: "test-signature", sender: "rpc", status, slot: status === "confirmed" ? 1 : null, ...(status === "confirmed" ? { feeLamports: 13000 } : {}) });
16
+ Object.defineProperty(core, "executions", { value: { get: () => ({ metaJson: null }) } });
17
+ return { core, prices, samples: () => samples };
18
+ }
19
+ const flags = (extra: Array<[string, string]> = []) => new Map([["wallet", "bags"], ["sol", "2"], ["live", "true"], ...extra]);
20
+ test("SOL transfer uses writable market fees and reports selected versus actual network fees", async () => {
21
+ const ctx = fixture();
22
+ const result = await runTransferCommand({ slrd: ctx.core, values: [Keypair.generate().publicKey.toBase58()], flags: flags() });
23
+ expect(ctx.prices).toEqual([800_000]); expect(ctx.samples()).toBe(1);
24
+ expect(result.status).toBe("confirmed"); expect(result.feeEstimate).toEqual({ cuLimit: 10_000, priorityMicroLamports: 800_000, priorityFeeLamports: 8000, estimatedBaseFeeLamports: 5000, estimatedNetworkFeeLamports: 13000 });
25
+ expect(result.feeLamports).toBe(13000);
26
+ });
27
+ test("explicit zero disables transfer fee estimation and pending outcome is never repeated blindly", async () => {
28
+ const ctx = fixture("submitted");
29
+ const result = await runTransferCommand({ slrd: ctx.core, values: [Keypair.generate().publicKey.toBase58()], flags: flags([["priority-micro-lamports", "0"]]) });
30
+ expect(ctx.prices).toEqual([0]); expect(ctx.samples()).toBe(0);
31
+ expect(result.status).toBe("unresolved"); expect(result.code).toBe("UNRESOLVED"); expect(result.feeLamports).toBeUndefined();
32
+ expect(result.feeEstimate).toMatchObject({ priorityFeeLamports: 0, estimatedNetworkFeeLamports: 5000 });
33
+ });