@solard/cli 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solard/cli",
3
- "version": "0.2.37",
3
+ "version": "0.2.40",
4
4
  "description": "Command-line interface for the Solard SDK.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -19,8 +19,8 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@solana/web3.js": "^1.98.4",
22
- "@solard/core": "0.2.37",
23
- "@solard/sdk": "0.2.37",
22
+ "@solard/core": "0.2.40",
23
+ "@solard/sdk": "0.2.40",
24
24
  "bs58": "^6.0.0",
25
25
  "measure-fn": "5.2.0"
26
26
  },
@@ -1,5 +1,7 @@
1
+ import { hasOutputMinimum, parseOutputMinimum } from "./trade-minimum.ts";
1
2
  import {
2
3
  executeJupiterSwap,
4
+ minimumBuyOutputForPrice,
3
5
  NATIVE_SOL_MINT,
4
6
  nativePumpTradeAvailable,
5
7
  quoteJupiterSwap,
@@ -74,7 +76,7 @@ export async function runSmartBuyCommand({
74
76
  }: RunSmartBuyCommandArgs): Promise<SmartBuyCommandResult> {
75
77
  const tokenRef = values[0];
76
78
  const usage =
77
- "Usage: slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) --sol <amount> [--venue auto|native|jupiter]";
79
+ "Usage: slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) --sol <amount> [--venue auto|native|jupiter] [--max-price-sol <SOL/token>] [--min-out <amount> | --min-out-raw <units> | --slippage-bps N]";
78
80
  if (!tokenRef) throw new Error(usage);
79
81
 
80
82
  const amountSol = flags.get("sol");
@@ -98,6 +100,20 @@ export async function runSmartBuyCommand({
98
100
  );
99
101
  }
100
102
 
103
+ const maxPriceSol = flags.get("max-price-sol");
104
+ if (flags.has("max-price-sol") && (!maxPriceSol || maxPriceSol === "true"))
105
+ throw new Error("--max-price-sol requires a positive SOL price per token");
106
+ if (maxPriceSol != null && flags.has("slippage-bps"))
107
+ throw new Error("Use --max-price-sol instead of --slippage-bps");
108
+ let outputMinimumRaw = parseOutputMinimum(flags, resolution.asset.decimals);
109
+ if (maxPriceSol != null) {
110
+ const priceMinimum = minimumBuyOutputForPrice(amount.raw, resolution.asset.decimals, maxPriceSol);
111
+ if (outputMinimumRaw == null || priceMinimum > outputMinimumRaw) outputMinimumRaw = priceMinimum;
112
+ }
113
+ if ((hasOutputMinimum(flags) || maxPriceSol != null) && resolution.route !== "native") {
114
+ throw new Error("Absolute minimum output is currently supported on native Pump/PumpSwap routes; Jupiter orders do not expose a verified on-chain minimum");
115
+ }
116
+
101
117
  if (resolution.route === "native") {
102
118
  if (!nativePumpTradeAvailable(resolution.token)) {
103
119
  throw new Error(
@@ -107,6 +123,8 @@ export async function runSmartBuyCommand({
107
123
 
108
124
  const options = {
109
125
  ...tradeFeeOptions(flags),
126
+ outputMinimumRaw,
127
+ maxPriceSol,
110
128
  slippageBps: intFlag(flags, "slippage-bps", 1_500),
111
129
  via: (flags.get("sender") ?? "rpc") as SenderId,
112
130
  skipSimulation: flags.has("skip-simulation"),
@@ -140,6 +158,8 @@ export async function runSmartBuyCommand({
140
158
  ...(await slrd.simulatePlan(prepared.plan)),
141
159
  priorityMicroLamports: prepared.priorityMicroLamports,
142
160
  cuLimit: prepared.plan.draft.cuLimit ?? 600_000,
161
+ requestedMinimumOutputRaw: outputMinimumRaw?.toString(),
162
+ guaranteedOutputRaw: prepared.plan.draft.actions.find(action => action.kind === "buy")?.meta?.minOutputRaw,
143
163
  };
144
164
  }),
145
165
  );
@@ -1,3 +1,4 @@
1
+ import { hasOutputMinimum, parseOutputMinimum } from "./trade-minimum.ts";
1
2
  import {
2
3
  executeJupiterSwap,
3
4
  NATIVE_SOL_MINT,
@@ -118,7 +119,7 @@ export async function runSmartSellCommand({
118
119
  }: RunSmartSellCommandArgs): Promise<SmartSellCommandResult> {
119
120
  const tokenRef = values[0];
120
121
  const usage =
121
- "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) [--bps 10000] [--venue auto|native|jupiter]";
122
+ "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) [--bps 10000] [--venue auto|native|jupiter] [--min-out <amount> | --min-out-raw <units> | --slippage-bps N]";
122
123
  if (!tokenRef) throw new Error(usage);
123
124
 
124
125
  const bps = intFlag(flags, "bps", 10_000);
@@ -135,6 +136,11 @@ export async function runSmartSellCommand({
135
136
  throw new Error("slrd sell expects an SPL token");
136
137
  }
137
138
 
139
+ const outputMinimumRaw = parseOutputMinimum(flags, 9);
140
+ if (hasOutputMinimum(flags) && resolution.route !== "native") {
141
+ throw new Error("Absolute minimum output is currently supported on native Pump/PumpSwap routes; Jupiter orders do not expose a verified on-chain minimum");
142
+ }
143
+
138
144
  if (resolution.route === "native") {
139
145
  if (!nativePumpTradeAvailable(resolution.token)) {
140
146
  throw new Error(
@@ -144,6 +150,7 @@ export async function runSmartSellCommand({
144
150
 
145
151
  const options = {
146
152
  ...tradeFeeOptions(flags),
153
+ outputMinimumRaw,
147
154
  bps,
148
155
  slippageBps: intFlag(flags, "slippage-bps", 1_500),
149
156
  via: (flags.get("sender") ?? "rpc") as SenderId,
@@ -178,6 +185,8 @@ export async function runSmartSellCommand({
178
185
  ...(await slrd.simulatePlan(prepared.plan)),
179
186
  priorityMicroLamports: prepared.priorityMicroLamports,
180
187
  cuLimit: prepared.plan.draft.cuLimit ?? 600_000,
188
+ requestedMinimumOutputRaw: outputMinimumRaw?.toString(),
189
+ guaranteedOutputRaw: prepared.plan.draft.actions.find(action => action.kind === "sell")?.meta?.minOutputRaw,
181
190
  };
182
191
  }),
183
192
  );
@@ -0,0 +1,50 @@
1
+ import { expect, test } from "bun:test";
2
+ import { parseOutputMinimum } from "./trade-minimum.ts";
3
+ import { runSmartBuyCommand } from "./buy.ts";
4
+ import { runSmartSellCommand } from "./sell.ts";
5
+ import type { Solard } from "@solard/core";
6
+ import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
7
+ const mint = "E4R6KvcnxvoHYBe872byyCeoyExGHja8n5Vo3JNWodus";
8
+ test("absolute output parsing preserves precision and rejects ambiguous or unsafe input", () => {
9
+ expect(parseOutputMinimum(new Map([["min-out", "123.000001"]]), 6)).toBe(123000001n);
10
+ expect(parseOutputMinimum(new Map([["min-out", "0.001"]]), 9)).toBe(1000000n);
11
+ expect(parseOutputMinimum(new Map([["min-out-raw", "9007199254740993"]]), 6)).toBe(9007199254740993n);
12
+ for (const value of ["true", "0", "-1", "1e3", "0.0000001", "18446744073709551616"]) {
13
+ expect(() => parseOutputMinimum(new Map([["min-out", value]]), 6)).toThrow();
14
+ }
15
+ expect(() => parseOutputMinimum(new Map([["min-out", "1"], ["slippage-bps", "100"]]), 6)).toThrow();
16
+ expect(() => parseOutputMinimum(new Map([["min-out", "1"], ["min-out-raw", "1"]]), 6)).toThrow();
17
+ });
18
+ test("CLI buy/sell forward per-wallet absolute output floors in correct units", async () => {
19
+ const seen: unknown[] = [];
20
+ const host = {
21
+ tokens: { list: () => [{ mint, decimals: 6, baseTokenProgram: TOKEN_PROGRAM_ID.toBase58(), venueHint: "pumpswap" }] },
22
+ buy: async (_mint: unknown, _wallet: unknown, _amount: unknown, options: unknown) => { seen.push(options); },
23
+ sell: async (_mint: unknown, _wallet: unknown, options: unknown) => { seen.push(options); },
24
+ } as unknown as Solard;
25
+ await runSmartBuyCommand({ slrd: host, values: [mint], flags: new Map([["wallet", "test"], ["sol", "0.001"], ["min-out", "123.5"]]) });
26
+ await runSmartSellCommand({ slrd: host, values: [mint], flags: new Map([["wallet", "test"], ["min-out", "0.001"]]) });
27
+ expect(seen[0]).toMatchObject({ outputMinimumRaw: 123500000n });
28
+ expect(seen[1]).toMatchObject({ outputMinimumRaw: 1000000n });
29
+ });
30
+ test("Jupiter absolute-floor requests fail explicitly without signing", async () => {
31
+ const host = { tokens: { list: () => [] }, signer: () => { throw new Error("SIGNER_ACCESSED"); } } as unknown as Solard;
32
+ await expect(runSmartBuyCommand({ slrd: host, values: ["USDC"],
33
+ flags: new Map([["wallet", "test"], ["sol", "0.001"], ["min-out", "1"]]),
34
+ })).rejects.toThrow("verified on-chain minimum");
35
+ });
36
+ test("maximum buy price derives the on-chain floor and combines with stricter token minima", async () => {
37
+ const seen: unknown[] = [];
38
+ const host = {
39
+ tokens: { list: () => [{ mint, decimals: 6, baseTokenProgram: TOKEN_PROGRAM_ID.toBase58(), venueHint: "pumpswap" }] },
40
+ buy: async (_mint: unknown, _wallet: unknown, _amount: unknown, options: unknown) => { seen.push(options); },
41
+ } as unknown as Solard;
42
+ const flags = new Map([["wallet", "test"], ["sol", "0.1"], ["max-price-sol", "0.0001"]]);
43
+ await runSmartBuyCommand({ slrd: host, values: [mint], flags });
44
+ expect(seen[0]).toMatchObject({ maxPriceSol: "0.0001", outputMinimumRaw: 1000000000n });
45
+ flags.set("min-out", "1100");
46
+ await runSmartBuyCommand({ slrd: host, values: [mint], flags });
47
+ expect(seen[1]).toMatchObject({ outputMinimumRaw: 1100000000n });
48
+ flags.delete("min-out"); flags.set("slippage-bps", "100");
49
+ await expect(runSmartBuyCommand({ slrd: host, values: [mint], flags })).rejects.toThrow("instead of");
50
+ });
@@ -0,0 +1,23 @@
1
+ import type { TradeCommandFlags } from "./trade-venue.ts";
2
+
3
+ export function hasOutputMinimum(flags: TradeCommandFlags): boolean {
4
+ return flags.has("min-out") || flags.has("min-out-raw");
5
+ }
6
+
7
+ export function parseOutputMinimum(flags: TradeCommandFlags, decimals: number): bigint | undefined {
8
+ if (!hasOutputMinimum(flags)) return undefined;
9
+ if (flags.has("slippage-bps")) throw new Error("Use --min-out or --min-out-raw instead of --slippage-bps");
10
+ if (flags.has("min-out") && flags.has("min-out-raw")) throw new Error("Choose --min-out or --min-out-raw");
11
+ const raw = flags.has("min-out-raw");
12
+ const value = flags.get(raw ? "min-out-raw" : "min-out") ?? "";
13
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 255)
14
+ throw new Error("Output mint decimals unavailable");
15
+ if (!(raw ? /^\d+$/ : /^\d+(?:\.\d+)?$/).test(value))
16
+ throw new Error("Minimum output must be a positive decimal amount (integer for --min-out-raw)");
17
+ const [whole, fraction = ""] = value.split(".");
18
+ if (!raw && fraction.length > decimals) throw new Error(`Minimum output supports at most ${decimals} decimal places`);
19
+ const amount = raw ? BigInt(value)
20
+ : BigInt(whole!) * 10n ** BigInt(decimals) + BigInt(fraction.padEnd(decimals, "0") || "0");
21
+ if (amount <= 0n || amount > 0xffffffffffffffffn) throw new Error("Minimum output must be a positive u64");
22
+ return amount;
23
+ }