@lightspeed-cli/speed-cli 0.1.0

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.
Files changed (74) hide show
  1. package/README.md +51 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +54 -0
  4. package/dist/commands/allowance.d.ts +2 -0
  5. package/dist/commands/allowance.js +37 -0
  6. package/dist/commands/approve.d.ts +2 -0
  7. package/dist/commands/approve.js +41 -0
  8. package/dist/commands/balance.d.ts +2 -0
  9. package/dist/commands/balance.js +100 -0
  10. package/dist/commands/bridge.d.ts +2 -0
  11. package/dist/commands/bridge.js +142 -0
  12. package/dist/commands/config.d.ts +11 -0
  13. package/dist/commands/config.js +83 -0
  14. package/dist/commands/dca.d.ts +2 -0
  15. package/dist/commands/dca.js +175 -0
  16. package/dist/commands/doctor.d.ts +2 -0
  17. package/dist/commands/doctor.js +113 -0
  18. package/dist/commands/estimate.d.ts +2 -0
  19. package/dist/commands/estimate.js +57 -0
  20. package/dist/commands/gas.d.ts +2 -0
  21. package/dist/commands/gas.js +128 -0
  22. package/dist/commands/history.d.ts +2 -0
  23. package/dist/commands/history.js +91 -0
  24. package/dist/commands/pending.d.ts +2 -0
  25. package/dist/commands/pending.js +125 -0
  26. package/dist/commands/price.d.ts +2 -0
  27. package/dist/commands/price.js +35 -0
  28. package/dist/commands/quote.d.ts +2 -0
  29. package/dist/commands/quote.js +54 -0
  30. package/dist/commands/revoke.d.ts +2 -0
  31. package/dist/commands/revoke.js +38 -0
  32. package/dist/commands/send.d.ts +2 -0
  33. package/dist/commands/send.js +43 -0
  34. package/dist/commands/setup.d.ts +2 -0
  35. package/dist/commands/setup.js +104 -0
  36. package/dist/commands/status.d.ts +2 -0
  37. package/dist/commands/status.js +64 -0
  38. package/dist/commands/swap.d.ts +2 -0
  39. package/dist/commands/swap.js +177 -0
  40. package/dist/commands/volume.d.ts +2 -0
  41. package/dist/commands/volume.js +241 -0
  42. package/dist/commands/whoami.d.ts +2 -0
  43. package/dist/commands/whoami.js +23 -0
  44. package/dist/commands/xp.d.ts +2 -0
  45. package/dist/commands/xp.js +56 -0
  46. package/dist/constants.d.ts +66 -0
  47. package/dist/constants.js +135 -0
  48. package/dist/env.d.ts +10 -0
  49. package/dist/env.js +58 -0
  50. package/dist/lib/alchemy-history.d.ts +24 -0
  51. package/dist/lib/alchemy-history.js +75 -0
  52. package/dist/lib/explorer.d.ts +24 -0
  53. package/dist/lib/explorer.js +59 -0
  54. package/dist/lib/oracle.d.ts +22 -0
  55. package/dist/lib/oracle.js +70 -0
  56. package/dist/lib/parse-amount.d.ts +8 -0
  57. package/dist/lib/parse-amount.js +19 -0
  58. package/dist/lib/pending-bridges.d.ts +12 -0
  59. package/dist/lib/pending-bridges.js +34 -0
  60. package/dist/lib/squid.d.ts +28 -0
  61. package/dist/lib/squid.js +23 -0
  62. package/dist/lib/swap-execute.d.ts +16 -0
  63. package/dist/lib/swap-execute.js +49 -0
  64. package/dist/lib/xp.d.ts +57 -0
  65. package/dist/lib/xp.js +217 -0
  66. package/dist/lib/zerox.d.ts +25 -0
  67. package/dist/lib/zerox.js +43 -0
  68. package/dist/output.d.ts +11 -0
  69. package/dist/output.js +46 -0
  70. package/dist/rpc.d.ts +5 -0
  71. package/dist/rpc.js +22 -0
  72. package/dist/wallet.d.ts +10 -0
  73. package/dist/wallet.js +28 -0
  74. package/package.json +46 -0
@@ -0,0 +1,49 @@
1
+ import { ethers } from "ethers";
2
+ import { get0xQuote } from "./zerox.js";
3
+ const ERC20_ABI = ["function approve(address spender, uint256 amount) returns (bool)"];
4
+ /**
5
+ * Execute swap from an existing quote: approve if needed → send transaction → wait.
6
+ * Used by swap command after it has already fetched the quote for display/prechecks.
7
+ */
8
+ export async function executeSwapWithQuote(signer, sellToken, quote, amountWei) {
9
+ if (!quote.transaction?.to) {
10
+ throw new Error("No transaction in quote (0x returned no fill).");
11
+ }
12
+ const data = quote.transaction.data?.trim();
13
+ if (!data || !data.startsWith("0x") || data.length < 10) {
14
+ throw new Error("Quote returned no or invalid transaction data (amount may be too small or unsupported by 0x).");
15
+ }
16
+ if (quote.issues?.allowance?.spender) {
17
+ const token = new ethers.Contract(sellToken, ERC20_ABI, signer);
18
+ const txApprove = await token.approve(quote.allowanceTarget ?? quote.issues.allowance.spender, amountWei);
19
+ await txApprove.wait();
20
+ }
21
+ try {
22
+ const tx = await signer.sendTransaction({
23
+ to: quote.transaction.to,
24
+ data,
25
+ value: quote.transaction.value ? BigInt(quote.transaction.value) : 0n,
26
+ gasLimit: quote.gas ? BigInt(quote.gas) : undefined,
27
+ });
28
+ await tx.wait();
29
+ return tx.hash;
30
+ }
31
+ catch (e) {
32
+ const err = e;
33
+ if (err?.code === "CALL_EXCEPTION" || (typeof err?.message === "string" && err.message.includes("revert"))) {
34
+ throw new Error("Transaction reverted on-chain (slippage, liquidity, or minimum amount). Try a larger amount or run again.");
35
+ }
36
+ throw e;
37
+ }
38
+ }
39
+ /**
40
+ * Execute one swap: get quote → approve if needed → send transaction → wait.
41
+ * Used by volume command. No confirmation, no dry-run.
42
+ * @returns { txHash, quote }
43
+ */
44
+ export async function executeSwap(chainId, signer, sellToken, buyToken, amountWei) {
45
+ const taker = await signer.getAddress();
46
+ const quote = await get0xQuote(chainId, sellToken, buyToken, amountWei, taker);
47
+ const txHash = await executeSwapWithQuote(signer, sellToken, quote, amountWei);
48
+ return { txHash, quote };
49
+ }
@@ -0,0 +1,57 @@
1
+ declare const XP_BASE: {
2
+ readonly swap: 10;
3
+ readonly bridge: 25;
4
+ readonly dcaBuy: 8;
5
+ readonly volumeOp: 3;
6
+ readonly gasRefuel: 3;
7
+ };
8
+ export type XpActionType = keyof typeof XP_BASE;
9
+ export interface XpStatsEntry {
10
+ count: number;
11
+ totalUSD: number;
12
+ }
13
+ export interface XpState {
14
+ totalXP: number;
15
+ level: number;
16
+ streak: number;
17
+ lastActivity: string;
18
+ stats: {
19
+ swaps: XpStatsEntry;
20
+ bridges: XpStatsEntry;
21
+ volumeOps: XpStatsEntry;
22
+ dcaBuys: XpStatsEntry;
23
+ gasRefuels: XpStatsEntry;
24
+ };
25
+ history: {
26
+ action: XpActionType;
27
+ xp: number;
28
+ usd: number;
29
+ at: string;
30
+ }[];
31
+ }
32
+ /** Level from total XP (1-based). */
33
+ export declare function getLevelFromTotalXp(totalXp: number): number;
34
+ /** XP needed to go from current level to next. */
35
+ export declare function getXpForNextLevel(level: number): number;
36
+ /** Progress within current level (0..1). */
37
+ export declare function getProgressInLevel(totalXp: number): {
38
+ current: number;
39
+ needed: number;
40
+ fraction: number;
41
+ };
42
+ /**
43
+ * Compute XP for one action. Formula: floor(base × dollarMod(usd) × streakMult).
44
+ * Same usd must be used when recording to totalUSD so XP and stats stay in sync.
45
+ */
46
+ export declare function computeXpForAction(action: XpActionType, usdAmount: number, streakDays: number): number;
47
+ export declare function getTitleForLevel(level: number): string;
48
+ export declare function loadXpState(): XpState;
49
+ /** Reset XP to zero (level 1, no stats, no history). Persists to disk. */
50
+ export declare function resetXpState(): void;
51
+ /**
52
+ * Record one action and persist. Call after successful swap/bridge/volume op/dca buy/gas.
53
+ * XP is computed from the same (clamped) usd that is added to stats.totalUSD, so displayed
54
+ * totals and XP always reflect the same executed value per action.
55
+ */
56
+ export declare function recordXpAction(action: XpActionType, usdAmount: number): number;
57
+ export {};
package/dist/lib/xp.js ADDED
@@ -0,0 +1,217 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { getSpeedConfigDir } from "../env.js";
4
+ const XP_BASE = {
5
+ swap: 10,
6
+ bridge: 25,
7
+ dcaBuy: 8,
8
+ volumeOp: 3,
9
+ gasRefuel: 3,
10
+ };
11
+ /** Sanitize totalUSD so obviously wrong stored values (e.g. wei as USD) don't display. */
12
+ function sanitizeTotalUsd(totalUSD) {
13
+ if (typeof totalUSD !== "number" || !Number.isFinite(totalUSD) || totalUSD < 0)
14
+ return 0;
15
+ if (totalUSD > 1e12)
16
+ return 0; // e.g. wei or wrong scale was stored
17
+ return totalUSD;
18
+ }
19
+ const DEFAULT_STATS = () => ({
20
+ swaps: { count: 0, totalUSD: 0 },
21
+ bridges: { count: 0, totalUSD: 0 },
22
+ volumeOps: { count: 0, totalUSD: 0 },
23
+ dcaBuys: { count: 0, totalUSD: 0 },
24
+ gasRefuels: { count: 0, totalUSD: 0 },
25
+ });
26
+ const HISTORY_MAX = 100;
27
+ function getXpPath() {
28
+ return join(getSpeedConfigDir(), "xp.json");
29
+ }
30
+ function todayISO() {
31
+ return new Date().toISOString().slice(0, 10);
32
+ }
33
+ function parseDate(s) {
34
+ const d = new Date(s);
35
+ return Number.isNaN(d.getTime()) ? new Date(0) : d;
36
+ }
37
+ function daysBetween(a, b) {
38
+ const d1 = parseDate(a).setHours(0, 0, 0, 0);
39
+ const d2 = parseDate(b).setHours(0, 0, 0, 0);
40
+ return Math.round((d2 - d1) / (24 * 60 * 60 * 1000));
41
+ }
42
+ /** Total XP required to reach level L (level 1 = 0 XP). */
43
+ function totalXpForLevel(level) {
44
+ if (level <= 1)
45
+ return 0;
46
+ let sum = 0;
47
+ for (let i = 1; i < level; i++) {
48
+ sum += Math.floor(500 * Math.pow(i, 1.4));
49
+ }
50
+ return sum;
51
+ }
52
+ /** Level from total XP (1-based). */
53
+ export function getLevelFromTotalXp(totalXp) {
54
+ if (totalXp <= 0)
55
+ return 1;
56
+ let level = 1;
57
+ while (totalXpForLevel(level + 1) <= totalXp)
58
+ level++;
59
+ return level;
60
+ }
61
+ /** XP needed to go from current level to next. */
62
+ export function getXpForNextLevel(level) {
63
+ return Math.floor(500 * Math.pow(level, 1.4));
64
+ }
65
+ /** Progress within current level (0..1). */
66
+ export function getProgressInLevel(totalXp) {
67
+ const level = getLevelFromTotalXp(totalXp);
68
+ const currentInLevel = totalXp - totalXpForLevel(level);
69
+ const needed = getXpForNextLevel(level);
70
+ return { current: currentInLevel, needed, fraction: needed > 0 ? currentInLevel / needed : 1 };
71
+ }
72
+ /** Dollar modifier: log10(dollars + 1), capped at 5. Min 0.15 so $0 actions give less (reduces low-value farming). */
73
+ function dollarMod(usd) {
74
+ const raw = Math.log10(usd + 1);
75
+ const capped = Math.min(5, Math.max(0.15, raw));
76
+ return capped;
77
+ }
78
+ /** Streak multiplier: 1.0 + (streakDays * 0.02), max 1.5. */
79
+ function streakMultiplier(streakDays) {
80
+ return Math.min(1.5, 1 + streakDays * 0.02);
81
+ }
82
+ /**
83
+ * Compute XP for one action. Formula: floor(base × dollarMod(usd) × streakMult).
84
+ * Same usd must be used when recording to totalUSD so XP and stats stay in sync.
85
+ */
86
+ export function computeXpForAction(action, usdAmount, streakDays) {
87
+ const base = XP_BASE[action];
88
+ const mod = dollarMod(usdAmount);
89
+ const streak = streakMultiplier(streakDays);
90
+ return Math.floor(base * mod * streak);
91
+ }
92
+ /** Deterministic silly title from level (never ends). */
93
+ const ADJECTIVES = [
94
+ "Speedy", "Blazing", "Cosmic", "Turbo", "Giga", "Hyper", "Ultra", "Chad", "Diamond", "Rocket",
95
+ "Phantom", "Ninja", "Stealth", "Smooth", "Alpha", "Sigma", "Prime", "Eternal", "Infinite", "Supreme",
96
+ "Mega", "Sonic", "Neo", "Cyber", "Quantum", "Atomic", "Plasma", "Crystal", "Obsidian", "Void",
97
+ "Storm", "Flame", "Frost", "Shadow", "Light", "Dawn", "Dusk", "Solar", "Lunar", "Stellar",
98
+ ];
99
+ const NOUNS = [
100
+ "Pilot", "Runner", "Hopper", "Racer", "Surfer", "Wizard", "Ape", "Degen", "Chad", "Whale",
101
+ "Dragon", "Phoenix", "Titan", "Legend", "Veteran", "Sage", "Mage", "Knight", "Captain", "Lord",
102
+ "Grinder", "Sprint", "Bolt", "Flash", "Blur", "Ghost", "Spirit", "Core", "Soul", "Heart",
103
+ "Chain", "Bridge", "Swap", "Flow", "Wave", "Pulse", "Spark", "Beam", "Ray", "Edge",
104
+ ];
105
+ export function getTitleForLevel(level) {
106
+ if (level < 1)
107
+ return "Rookie";
108
+ const a = (level * 7) % ADJECTIVES.length;
109
+ const b = (level * 11) % NOUNS.length;
110
+ return `${ADJECTIVES[a]} ${NOUNS[b]}`;
111
+ }
112
+ export function loadXpState() {
113
+ const path = getXpPath();
114
+ try {
115
+ if (existsSync(path)) {
116
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
117
+ const stats = raw.stats;
118
+ const state = {
119
+ totalXP: Number(raw.totalXP) || 0,
120
+ level: Number(raw.level) || 1,
121
+ streak: Number(raw.streak) || 0,
122
+ lastActivity: typeof raw.lastActivity === "string" ? raw.lastActivity : "",
123
+ stats: stats
124
+ ? {
125
+ swaps: { count: stats.swaps?.count ?? 0, totalUSD: sanitizeTotalUsd(stats.swaps?.totalUSD ?? 0) },
126
+ bridges: { count: stats.bridges?.count ?? 0, totalUSD: sanitizeTotalUsd(stats.bridges?.totalUSD ?? 0) },
127
+ volumeOps: { count: stats.volumeOps?.count ?? 0, totalUSD: sanitizeTotalUsd(stats.volumeOps?.totalUSD ?? 0) },
128
+ dcaBuys: { count: stats.dcaBuys?.count ?? 0, totalUSD: sanitizeTotalUsd(stats.dcaBuys?.totalUSD ?? 0) },
129
+ gasRefuels: { count: stats.gasRefuels?.count ?? 0, totalUSD: sanitizeTotalUsd(stats.gasRefuels?.totalUSD ?? 0) },
130
+ }
131
+ : DEFAULT_STATS(),
132
+ history: Array.isArray(raw.history) ? raw.history.slice(-HISTORY_MAX) : [],
133
+ };
134
+ // If any totalUSD was sanitized (corrupted), persist so file is repaired
135
+ const hadCorruption = stats &&
136
+ (sanitizeTotalUsd(stats.volumeOps?.totalUSD ?? 0) !== (stats.volumeOps?.totalUSD ?? 0) ||
137
+ sanitizeTotalUsd(stats.swaps?.totalUSD ?? 0) !== (stats.swaps?.totalUSD ?? 0) ||
138
+ sanitizeTotalUsd(stats.bridges?.totalUSD ?? 0) !== (stats.bridges?.totalUSD ?? 0) ||
139
+ sanitizeTotalUsd(stats.dcaBuys?.totalUSD ?? 0) !== (stats.dcaBuys?.totalUSD ?? 0) ||
140
+ sanitizeTotalUsd(stats.gasRefuels?.totalUSD ?? 0) !== (stats.gasRefuels?.totalUSD ?? 0));
141
+ if (hadCorruption)
142
+ saveXpState(state);
143
+ return state;
144
+ }
145
+ }
146
+ catch (_) { }
147
+ return {
148
+ totalXP: 0,
149
+ level: 1,
150
+ streak: 0,
151
+ lastActivity: "",
152
+ stats: DEFAULT_STATS(),
153
+ history: [],
154
+ };
155
+ }
156
+ function saveXpState(state) {
157
+ mkdirSync(getSpeedConfigDir(), { recursive: true });
158
+ writeFileSync(getXpPath(), JSON.stringify(state, null, 2));
159
+ }
160
+ /** Reset XP to zero (level 1, no stats, no history). Persists to disk. */
161
+ export function resetXpState() {
162
+ const state = {
163
+ totalXP: 0,
164
+ level: 1,
165
+ streak: 0,
166
+ lastActivity: "",
167
+ stats: DEFAULT_STATS(),
168
+ history: [],
169
+ };
170
+ saveXpState(state);
171
+ }
172
+ /** Clamp USD to a sane range so bad oracle/scale never corrupts totals (e.g. wei passed as USD). */
173
+ function clampUsdForRecord(usd) {
174
+ if (typeof usd !== "number" || !Number.isFinite(usd) || usd < 0)
175
+ return 0;
176
+ return Math.min(usd, 1e9); // max $1B per single action
177
+ }
178
+ /**
179
+ * Record one action and persist. Call after successful swap/bridge/volume op/dca buy/gas.
180
+ * XP is computed from the same (clamped) usd that is added to stats.totalUSD, so displayed
181
+ * totals and XP always reflect the same executed value per action.
182
+ */
183
+ export function recordXpAction(action, usdAmount) {
184
+ const state = loadXpState();
185
+ const today = todayISO();
186
+ const usd = clampUsdForRecord(usdAmount);
187
+ let streak = state.streak;
188
+ if (state.lastActivity) {
189
+ const days = daysBetween(state.lastActivity, today);
190
+ if (days === 0) {
191
+ // same day, keep streak
192
+ }
193
+ else if (days === 1) {
194
+ streak = state.streak + 1;
195
+ }
196
+ else {
197
+ streak = 1;
198
+ }
199
+ }
200
+ else {
201
+ streak = 1;
202
+ }
203
+ const xp = computeXpForAction(action, usd, streak);
204
+ state.totalXP += xp;
205
+ state.level = getLevelFromTotalXp(state.totalXP);
206
+ state.streak = streak;
207
+ state.lastActivity = today;
208
+ const statKey = action === "volumeOp" ? "volumeOps" : action === "dcaBuy" ? "dcaBuys" : action === "gasRefuel" ? "gasRefuels" : action === "swap" ? "swaps" : "bridges";
209
+ const entry = state.stats[statKey];
210
+ entry.count += 1;
211
+ entry.totalUSD += usd;
212
+ state.history.push({ action, xp, usd, at: new Date().toISOString() });
213
+ if (state.history.length > HISTORY_MAX)
214
+ state.history = state.history.slice(-HISTORY_MAX);
215
+ saveXpState(state);
216
+ return xp;
217
+ }
@@ -0,0 +1,25 @@
1
+ export interface ZeroXQuoteResult {
2
+ sellAmount: string;
3
+ buyAmount: string;
4
+ sellToken: string;
5
+ buyToken: string;
6
+ gas?: string;
7
+ transaction?: {
8
+ to: string;
9
+ data: string;
10
+ value: string;
11
+ gas?: string;
12
+ };
13
+ allowanceTarget?: string;
14
+ issues?: {
15
+ allowance?: {
16
+ spender: string;
17
+ };
18
+ balance?: unknown;
19
+ };
20
+ }
21
+ export declare function get0xPrice(chainId: number, sellToken: string, buyToken: string, sellAmount: string, taker: string): Promise<{
22
+ buyAmount: string;
23
+ sellAmount: string;
24
+ }>;
25
+ export declare function get0xQuote(chainId: number, sellToken: string, buyToken: string, sellAmount: string, taker: string): Promise<ZeroXQuoteResult>;
@@ -0,0 +1,43 @@
1
+ import { ZEROX_API_BASE } from "../constants.js";
2
+ import { getEnvPath } from "../env.js";
3
+ function get0xApiKey() {
4
+ const key = process.env["OX_API_KEY"] ?? process.env["0X_API_KEY"];
5
+ if (!key?.trim()) {
6
+ throw new Error(`0X_API_KEY or OX_API_KEY must be set in ${getEnvPath()}. Run 'speed setup' to configure.`);
7
+ }
8
+ return key.trim();
9
+ }
10
+ function getHeaders() {
11
+ return {
12
+ "0x-api-key": get0xApiKey(),
13
+ "0x-version": "v2",
14
+ "Content-Type": "application/json",
15
+ };
16
+ }
17
+ export async function get0xPrice(chainId, sellToken, buyToken, sellAmount, taker) {
18
+ const url = `${ZEROX_API_BASE}/swap/allowance-holder/price?chainId=${chainId}&sellToken=${sellToken}&buyToken=${buyToken}&sellAmount=${sellAmount}&taker=${taker}`;
19
+ const res = await fetch(url, { headers: getHeaders() });
20
+ if (!res.ok) {
21
+ const text = await res.text();
22
+ throw new Error(`0x price failed: ${res.status} ${text}`);
23
+ }
24
+ const data = (await res.json());
25
+ return { buyAmount: data.buyAmount, sellAmount: data.sellAmount };
26
+ }
27
+ export async function get0xQuote(chainId, sellToken, buyToken, sellAmount, taker) {
28
+ const params = new URLSearchParams({
29
+ chainId: String(chainId),
30
+ sellToken,
31
+ buyToken,
32
+ sellAmount,
33
+ taker,
34
+ });
35
+ const url = `${ZEROX_API_BASE}/swap/allowance-holder/quote?${params}`;
36
+ const res = await fetch(url, { headers: getHeaders() });
37
+ if (!res.ok) {
38
+ const text = await res.text();
39
+ throw new Error(`0x quote failed: ${res.status} ${text}`);
40
+ }
41
+ const data = (await res.json());
42
+ return data;
43
+ }
@@ -0,0 +1,11 @@
1
+ export declare function setJsonMode(value: boolean): void;
2
+ export declare function isJsonMode(): boolean;
3
+ /** Write to stdout (or JSON when --json). Human messages: use out.* or console.log. Errors: use err.* or console.error. */
4
+ export declare function out(obj: unknown): void;
5
+ export declare function err(message: string): void;
6
+ export declare function success(message: string): void;
7
+ export declare function warn(message: string): void;
8
+ /** Suffix for errors: " Run 'speed <cmd> --help' for usage." (omit when message already includes usage). */
9
+ export declare function usageHint(cmd: string): string;
10
+ /** Exit with code 1; in --json mode print { error, code } to stdout, else message to stderr */
11
+ export declare function exitWithError(message: string, code: string): never;
package/dist/output.js ADDED
@@ -0,0 +1,46 @@
1
+ import chalk from "chalk";
2
+ let jsonMode = false;
3
+ export function setJsonMode(value) {
4
+ jsonMode = value;
5
+ }
6
+ export function isJsonMode() {
7
+ return jsonMode;
8
+ }
9
+ /** Write to stdout (or JSON when --json). Human messages: use out.* or console.log. Errors: use err.* or console.error. */
10
+ export function out(obj) {
11
+ if (jsonMode) {
12
+ console.log(JSON.stringify(obj));
13
+ }
14
+ else {
15
+ console.log(obj);
16
+ }
17
+ }
18
+ export function err(message) {
19
+ if (!jsonMode) {
20
+ console.error(chalk.red(message));
21
+ }
22
+ }
23
+ export function success(message) {
24
+ if (jsonMode)
25
+ return;
26
+ console.log(chalk.green(message));
27
+ }
28
+ export function warn(message) {
29
+ if (jsonMode)
30
+ return;
31
+ console.error(chalk.yellow(message));
32
+ }
33
+ /** Suffix for errors: " Run 'speed <cmd> --help' for usage." (omit when message already includes usage). */
34
+ export function usageHint(cmd) {
35
+ return ` Run 'speed ${cmd} --help' for usage.`;
36
+ }
37
+ /** Exit with code 1; in --json mode print { error, code } to stdout, else message to stderr */
38
+ export function exitWithError(message, code) {
39
+ if (jsonMode) {
40
+ console.log(JSON.stringify({ error: message, code }));
41
+ }
42
+ else {
43
+ console.error(chalk.red(message));
44
+ }
45
+ process.exit(1);
46
+ }
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Get RPC URL for a chain: Alchemy when ALCHEMY_API_KEY is set, else one public fallback.
3
+ * Base (8453) can override with RPC_URL env. No health checks or caching.
4
+ */
5
+ export declare function getRpcUrl(chainId: number): string;
package/dist/rpc.js ADDED
@@ -0,0 +1,22 @@
1
+ import { ALCHEMY_CHAIN_PREFIX, PUBLIC_RPC_FALLBACKS } from "./constants.js";
2
+ /**
3
+ * Get RPC URL for a chain: Alchemy when ALCHEMY_API_KEY is set, else one public fallback.
4
+ * Base (8453) can override with RPC_URL env. No health checks or caching.
5
+ */
6
+ export function getRpcUrl(chainId) {
7
+ if (chainId === 8453) {
8
+ const custom = process.env.RPC_URL;
9
+ if (custom?.trim())
10
+ return custom.trim();
11
+ }
12
+ const apiKey = process.env.ALCHEMY_API_KEY?.trim();
13
+ if (apiKey) {
14
+ const prefix = ALCHEMY_CHAIN_PREFIX[chainId];
15
+ if (prefix)
16
+ return `https://${prefix}-mainnet.g.alchemy.com/v2/${apiKey}`;
17
+ }
18
+ const fallback = PUBLIC_RPC_FALLBACKS[chainId];
19
+ if (fallback)
20
+ return fallback;
21
+ throw new Error(`Unsupported chain ID: ${chainId}`);
22
+ }
@@ -0,0 +1,10 @@
1
+ import { ethers } from "ethers";
2
+ /**
3
+ * Create an ethers Wallet (signer) for the given chain using PRIVATE_KEY and getRpcUrl(chainId).
4
+ */
5
+ export declare function getSigner(chainId: number): ethers.Wallet;
6
+ /**
7
+ * Get the address derived from PRIVATE_KEY without creating a provider.
8
+ */
9
+ export declare function getAddress(): string;
10
+ export declare function getPrivateKeyForValidation(): string;
package/dist/wallet.js ADDED
@@ -0,0 +1,28 @@
1
+ import { ethers } from "ethers";
2
+ import { getRpcUrl } from "./rpc.js";
3
+ function getPrivateKey() {
4
+ const key = process.env.PRIVATE_KEY ?? process.env.SPEED_PRIVATE_KEY;
5
+ if (!key?.trim()) {
6
+ throw new Error("PRIVATE_KEY or SPEED_PRIVATE_KEY must be set in environment");
7
+ }
8
+ return key.startsWith("0x") ? key : `0x${key}`;
9
+ }
10
+ /**
11
+ * Create an ethers Wallet (signer) for the given chain using PRIVATE_KEY and getRpcUrl(chainId).
12
+ */
13
+ export function getSigner(chainId) {
14
+ const key = getPrivateKey();
15
+ const url = getRpcUrl(chainId);
16
+ const provider = new ethers.JsonRpcProvider(url);
17
+ return new ethers.Wallet(key, provider);
18
+ }
19
+ /**
20
+ * Get the address derived from PRIVATE_KEY without creating a provider.
21
+ */
22
+ export function getAddress() {
23
+ const key = getPrivateKey();
24
+ return new ethers.Wallet(key).address;
25
+ }
26
+ export function getPrivateKeyForValidation() {
27
+ return getPrivateKey();
28
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@lightspeed-cli/speed-cli",
3
+ "version": "0.1.0",
4
+ "description": "Speed Token CLI: swap, bridge, balance, price, volume, DCA, gas, XP. Uses 0x and Squid; config in ~/.speed.",
5
+ "type": "module",
6
+ "bin": {
7
+ "speed": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/cli.js",
15
+ "dev": "tsc --watch",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "keywords": [
22
+ "speed",
23
+ "token",
24
+ "cli",
25
+ "swap",
26
+ "bridge",
27
+ "0x",
28
+ "squid",
29
+ "ethereum",
30
+ "base"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@0xsquid/sdk": "^2.2.0",
35
+ "@0xsquid/squid-types": "^0.1.215",
36
+ "chalk": "^5.3.0",
37
+ "commander": "^12.0.0",
38
+ "dotenv": "^16.4.5",
39
+ "ethers": "^6.13.0",
40
+ "ora": "^8.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.11.0",
44
+ "typescript": "^5.3.0"
45
+ }
46
+ }