@solard/cli 0.2.12 → 0.2.14

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.
@@ -0,0 +1,239 @@
1
+ import {
2
+ executeJupiterSwap,
3
+ NATIVE_SOL_MINT,
4
+ nativePumpTradeAvailable,
5
+ quoteJupiterSwap,
6
+ resolveTradeRoute,
7
+ type SenderId,
8
+ type Solard,
9
+ } from "@solard/sdk";
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
+ }
@@ -0,0 +1,19 @@
1
+ import type { TradeAsset, TradeRoute, TradeRouteResolution } from "@solard/sdk";
2
+
3
+ export type TradeRouteSummary = {
4
+ route: TradeRoute;
5
+ asset: TradeAsset;
6
+ registered: boolean;
7
+ venue: string | null;
8
+ };
9
+
10
+ export function summarizeTradeRoute(
11
+ resolution: TradeRouteResolution,
12
+ ): TradeRouteSummary {
13
+ return {
14
+ route: resolution.route,
15
+ asset: resolution.asset,
16
+ registered: resolution.token !== null,
17
+ venue: resolution.token?.venueHint ?? null,
18
+ };
19
+ }
@@ -0,0 +1,43 @@
1
+ import type { Solard } from "@solard/sdk";
2
+ import type { TradeCommandFlags } from "./trade-venue.ts";
3
+
4
+ export type TradeTargets = {
5
+ mode: "wallet" | "wallets" | "group";
6
+ refs: string[];
7
+ group?: string;
8
+ };
9
+
10
+ function csv(value: string | undefined): string[] {
11
+ return (value ?? "")
12
+ .split(",")
13
+ .map((item) => item.trim())
14
+ .filter(Boolean);
15
+ }
16
+
17
+ export function resolveTradeTargets(
18
+ slrd: Pick<Solard, "groupWallets">,
19
+ flags: TradeCommandFlags,
20
+ usage: string,
21
+ ): TradeTargets {
22
+ const wallet = flags.get("wallet");
23
+ const wallets = csv(flags.get("wallets"));
24
+ const group = flags.get("group");
25
+ const selected = [Boolean(wallet), wallets.length > 0, Boolean(group)].filter(
26
+ Boolean,
27
+ ).length;
28
+
29
+ if (selected !== 1) {
30
+ throw new Error(
31
+ `${usage} Supply exactly one of --wallet, --wallets, or --group.`,
32
+ );
33
+ }
34
+
35
+ if (wallet) return { mode: "wallet", refs: [wallet] };
36
+ if (wallets.length > 0) return { mode: "wallets", refs: wallets };
37
+
38
+ return {
39
+ mode: "group",
40
+ refs: slrd.groupWallets(group!).map((ref) => String(ref)),
41
+ group,
42
+ };
43
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseVenuePreference } from "./trade-venue.ts";
3
+
4
+ function flags(input: Record<string, string>): ReadonlyMap<string, string> {
5
+ return new Map(Object.entries(input));
6
+ }
7
+
8
+ describe("parseVenuePreference", () => {
9
+ test("defaults to auto", () => {
10
+ expect(parseVenuePreference(flags({}))).toBe("auto");
11
+ });
12
+
13
+ test("parses --venue", () => {
14
+ expect(parseVenuePreference(flags({ venue: "native" }))).toBe("native");
15
+ expect(parseVenuePreference(flags({ venue: "JUPITER" }))).toBe("jupiter");
16
+ });
17
+
18
+ test("legacy shortcut flags override --venue", () => {
19
+ expect(
20
+ parseVenuePreference(flags({ venue: "native", jupiter: "true" })),
21
+ ).toBe("jupiter");
22
+ expect(
23
+ parseVenuePreference(flags({ venue: "jupiter", native: "true" })),
24
+ ).toBe("native");
25
+ });
26
+
27
+ test("rejects invalid venue", () => {
28
+ expect(() => parseVenuePreference(flags({ venue: "raydium" }))).toThrow(
29
+ "--venue must be auto, native, or jupiter",
30
+ );
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ import type { VenuePreference } from "@solard/sdk";
2
+
3
+ export type TradeCommandFlags = ReadonlyMap<string, string>;
4
+
5
+ export function parseVenuePreference(
6
+ flags: TradeCommandFlags,
7
+ ): VenuePreference {
8
+ const value = flags.has("jupiter")
9
+ ? "jupiter"
10
+ : flags.has("native")
11
+ ? "native"
12
+ : (flags.get("venue") ?? "auto").toLowerCase();
13
+
14
+ if (value !== "auto" && value !== "native" && value !== "jupiter") {
15
+ throw new Error("--venue must be auto, native, or jupiter");
16
+ }
17
+
18
+ return value;
19
+ }
@@ -0,0 +1,200 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import {
3
+ readMint,
4
+ sol,
5
+ tokenAmount,
6
+ transferTokenIxs,
7
+ type SenderId,
8
+ type Solard,
9
+ } from "@solard/sdk";
10
+
11
+ import { resolveDestinationRef } from "../refs.ts";
12
+
13
+ const CANONICAL_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
14
+
15
+ export type TransferCliFlags = ReadonlyMap<string, string>;
16
+
17
+ type RunTransferCommandArgs = {
18
+ slrd: Solard;
19
+ values: string[];
20
+ flags: TransferCliFlags;
21
+ };
22
+
23
+ function requiredFlag(flags: TransferCliFlags, key: string): string {
24
+ const value = flags.get(key);
25
+ if (!value || value === "true") {
26
+ throw new Error(`Missing --${key} <value>`);
27
+ }
28
+ return value;
29
+ }
30
+
31
+ function senderId(flags: TransferCliFlags): SenderId {
32
+ const value = flags.get("sender") ?? "rpc";
33
+ if (value !== "rpc" && value !== "helius" && value !== "jito") {
34
+ throw new Error(`Invalid --sender: ${value}`);
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function nonNegativeInteger(
40
+ flags: TransferCliFlags,
41
+ key: string,
42
+ fallback: number,
43
+ ): number {
44
+ const raw = flags.get(key);
45
+ const value = raw == null ? fallback : Number(raw);
46
+ if (!Number.isInteger(value) || value < 0) {
47
+ throw new Error(`--${key} must be a non-negative integer`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function positiveInteger(
53
+ flags: TransferCliFlags,
54
+ key: string,
55
+ fallback: number,
56
+ ): number {
57
+ const value = nonNegativeInteger(flags, key, fallback);
58
+ if (value <= 0) throw new Error(`--${key} must be a positive integer`);
59
+ return value;
60
+ }
61
+
62
+ export async function runTransferCommand({
63
+ slrd,
64
+ values,
65
+ flags,
66
+ }: RunTransferCommandArgs): Promise<Record<string, unknown>> {
67
+ const recipientInput = values[0] === "sol" ? values[1] : values[0];
68
+ if (!recipientInput) {
69
+ throw new Error(
70
+ "Usage: slrd transfer <contact|wallet|address> --wallet <wallet> (--sol <amount> | --token <USDC|mint> --amount <ui>) [--sender rpc|helius|jito] [--simulate-only]",
71
+ );
72
+ }
73
+
74
+ const recipient = resolveDestinationRef(slrd, recipientInput);
75
+ const wallet = requiredFlag(flags, "wallet");
76
+ const via = senderId(flags);
77
+ const tokenRef = flags.get("token")?.trim();
78
+ const solValue = flags.get("sol")?.trim();
79
+
80
+ if (Boolean(tokenRef) === Boolean(solValue)) {
81
+ throw new Error(
82
+ "Transfer requires exactly one of --sol <amount> or --token <USDC|mint> --amount <ui>",
83
+ );
84
+ }
85
+
86
+ const cuLimit = positiveInteger(
87
+ flags,
88
+ "cu-limit",
89
+ tokenRef ? 30_000 : 10_000,
90
+ );
91
+ const priorityMicroLamports = nonNegativeInteger(
92
+ flags,
93
+ "priority-micro-lamports",
94
+ 0,
95
+ );
96
+
97
+ if (tokenRef) {
98
+ const amountUi = requiredFlag(flags, "amount");
99
+ const normalizedToken = tokenRef.replace(/^\$/, "").trim();
100
+ const isUsdc = normalizedToken.toUpperCase() === "USDC";
101
+ const mint = new PublicKey(isUsdc ? CANONICAL_USDC_MINT : normalizedToken);
102
+ const mintState = await readMint(slrd.connection(), mint, slrd.cache);
103
+ const amountRaw = tokenAmount(
104
+ amountUi,
105
+ mint,
106
+ mintState.decimals,
107
+ mintState.tokenProgram,
108
+ ).raw;
109
+ if (amountRaw <= 0n) {
110
+ throw new Error("Token transfer amount must be greater than zero");
111
+ }
112
+
113
+ const owner = slrd.signer(wallet).publicKey;
114
+ const destinationOwner = new PublicKey(recipient.address);
115
+ const transfer = transferTokenIxs({
116
+ payer: owner,
117
+ owner,
118
+ recipient: destinationOwner,
119
+ mint,
120
+ amountRaw,
121
+ decimals: mintState.decimals,
122
+ tokenProgram: mintState.tokenProgram,
123
+ });
124
+
125
+ const composer = slrd
126
+ .tx(wallet)
127
+ .addMany(transfer.instructions, {
128
+ kind: "transfer-token",
129
+ mint,
130
+ recipient: destinationOwner,
131
+ meta: { raw: amountRaw.toString() },
132
+ })
133
+ .track({
134
+ address: transfer.destination,
135
+ kind: "token",
136
+ mint,
137
+ })
138
+ .priorityFee({ cuLimit, microLamports: priorityMicroLamports });
139
+
140
+ if (flags.has("simulate-only")) {
141
+ const result = await slrd.simulatePlan(await composer.build());
142
+ return {
143
+ mode: "simulation",
144
+ wallet,
145
+ recipient,
146
+ token: isUsdc ? "USDC" : mint.toBase58(),
147
+ mint: mint.toBase58(),
148
+ amount: amountUi,
149
+ decimals: mintState.decimals,
150
+ amountRaw,
151
+ result,
152
+ };
153
+ }
154
+
155
+ const receipt = await composer.send({
156
+ via,
157
+ kind: "transfer-token",
158
+ skipSimulation: flags.has("skip-simulation"),
159
+ skipPreflight:
160
+ flags.has("skip-preflight") || flags.has("skip-simulation"),
161
+ });
162
+
163
+ return {
164
+ ...receipt,
165
+ wallet,
166
+ recipient,
167
+ token: isUsdc ? "USDC" : mint.toBase58(),
168
+ mint: mint.toBase58(),
169
+ amount: amountUi,
170
+ decimals: mintState.decimals,
171
+ amountRaw,
172
+ };
173
+ }
174
+
175
+ const amount = solValue!;
176
+ const composer = slrd
177
+ .tx(wallet)
178
+ .transferSol(recipient.address, sol(amount))
179
+ .priorityFee({ cuLimit, microLamports: priorityMicroLamports });
180
+
181
+ if (flags.has("simulate-only")) {
182
+ const result = await slrd.simulatePlan(await composer.build());
183
+ return {
184
+ mode: "simulation",
185
+ wallet,
186
+ recipient,
187
+ sol: amount,
188
+ result,
189
+ };
190
+ }
191
+
192
+ const receipt = await composer.send({
193
+ via,
194
+ kind: "transfer-sol",
195
+ skipSimulation: flags.has("skip-simulation"),
196
+ skipPreflight: flags.has("skip-preflight") || flags.has("skip-simulation"),
197
+ });
198
+
199
+ return { ...receipt, recipient };
200
+ }