@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,177 @@
1
+ import { Command } from "commander";
2
+ import { createInterface } from "readline";
3
+ import ora from "ora";
4
+ import { ethers } from "ethers";
5
+ import { getSigner } from "../wallet.js";
6
+ import { get0xQuote } from "../lib/zerox.js";
7
+ import { executeSwapWithQuote } from "../lib/swap-execute.js";
8
+ import { getEthUsdPriceNumber, getSpeedUsdPriceNumber } from "../lib/oracle.js";
9
+ import { recordXpAction } from "../lib/xp.js";
10
+ import { parseTokenAmountToWei } from "../lib/parse-amount.js";
11
+ import { EXPLORER_URLS, resolveChainId, getChainOptionsHint, SPEED_TOKEN_ADDRESS, NATIVE_ETH_0X, CHAIN_NAMES, NATIVE_SYMBOL, } from "../constants.js";
12
+ import { getDefaultChainInput } from "./config.js";
13
+ import { out, exitWithError, setJsonMode, isJsonMode, success, usageHint } from "../output.js";
14
+ function askConfirm(prompt) {
15
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
16
+ return new Promise((resolve) => {
17
+ rl.question(prompt, (answer) => {
18
+ rl.close();
19
+ resolve(/^y|yes$/i.test(answer.trim()));
20
+ });
21
+ });
22
+ }
23
+ function tokenSymbol(addr, chainId) {
24
+ if (addr.toLowerCase() === NATIVE_ETH_0X.toLowerCase())
25
+ return NATIVE_SYMBOL[chainId] ?? "ETH";
26
+ if (addr.toLowerCase() === SPEED_TOKEN_ADDRESS.toLowerCase())
27
+ return "SPEED";
28
+ return "token";
29
+ }
30
+ function chainDisplayName(chainId) {
31
+ const name = CHAIN_NAMES[chainId] ?? String(chainId);
32
+ return name.charAt(0).toUpperCase() + name.slice(1);
33
+ }
34
+ /** Format wei as human string (assumes 18 decimals). */
35
+ function formatAmount(wei) {
36
+ try {
37
+ const n = ethers.formatEther(wei);
38
+ return parseFloat(n).toLocaleString(undefined, { maximumFractionDigits: 6 });
39
+ }
40
+ catch {
41
+ return wei;
42
+ }
43
+ }
44
+ export function swapCmd() {
45
+ return new Command("swap")
46
+ .description("One-shot swap: get quote → confirm → approve (if needed) → execute. No need to run quote or approve separately.")
47
+ .option("-c, --chain <id|name>", "Chain ID or name (e.g. 8453, base)", "8453")
48
+ .option("--sell <address>", "Sell token (default: Speed)")
49
+ .option("--buy <address>", "Buy token (default: Speed)")
50
+ .option("-a, --amount <amount>", "Sell amount in token units (e.g. 0.002, 10000, or 0.002 eth / 100 speed)")
51
+ .option("--go", "Skip confirmation and execute swap")
52
+ .option("--dry-run", "Only fetch and print quote; no approval or swap tx")
53
+ .action(async function (opts) {
54
+ setJsonMode(this.parent?.opts().json ?? false);
55
+ const yes = this.parent?.opts().yes ?? opts.go ?? false;
56
+ const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
57
+ if (chainId === null) {
58
+ exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}.${usageHint("swap")}`, "INVALID_CHAIN");
59
+ }
60
+ if (!opts.amount) {
61
+ exitWithError("--amount is required. Example: speed swap -c base -a 0.002", "MISSING_ARGS");
62
+ }
63
+ const toToken = (v, defaultVal) => {
64
+ if (!v)
65
+ return defaultVal;
66
+ const t = v.trim().toLowerCase();
67
+ if (t === "speed")
68
+ return SPEED_TOKEN_ADDRESS;
69
+ if (t === "eth" || t === "ether" || t === "native")
70
+ return NATIVE_ETH_0X;
71
+ return v.trim();
72
+ };
73
+ // Default: sell native ETH, buy Speed (so "swap -a 0.002" = sell 0.002 ETH for Speed)
74
+ const sellToken = toToken(opts.sell, NATIVE_ETH_0X);
75
+ const buyToken = toToken(opts.buy, SPEED_TOKEN_ADDRESS);
76
+ if (sellToken.toLowerCase() === buyToken.toLowerCase()) {
77
+ exitWithError("Sell and buy token must be different. Use --sell and/or --buy to specify.", "SAME_TOKEN");
78
+ }
79
+ const amount = parseTokenAmountToWei(opts.amount.trim());
80
+ try {
81
+ const signer = getSigner(chainId);
82
+ const taker = await signer.getAddress();
83
+ const spinner = isJsonMode() ? null : ora("Fetching quote...").start();
84
+ const quote = await get0xQuote(chainId, sellToken, buyToken, amount, taker);
85
+ if (spinner)
86
+ spinner.succeed("Quote received");
87
+ if (!quote.transaction?.to || !quote.transaction?.data) {
88
+ exitWithError("No transaction in quote (0x returned no fill). Try a different amount or token." + usageHint("swap"), "QUOTE_ERROR");
89
+ }
90
+ let gasEth = "0";
91
+ let gasUsd = "0";
92
+ let gasCostWei = 0n;
93
+ try {
94
+ const gasLimit = quote.gas ? BigInt(quote.gas) : 200000n;
95
+ const feeData = await signer.provider.getFeeData();
96
+ const gasPrice = feeData.gasPrice ?? 0n;
97
+ gasCostWei = gasLimit * gasPrice;
98
+ gasEth = ethers.formatEther(gasCostWei);
99
+ const ethUsd = await getEthUsdPriceNumber(chainId);
100
+ gasUsd = (parseFloat(gasEth) * ethUsd).toFixed(2);
101
+ }
102
+ catch (_) { }
103
+ const isSellingNativeEth = sellToken.toLowerCase() === NATIVE_ETH_0X.toLowerCase();
104
+ if (isSellingNativeEth) {
105
+ const balance = await signer.provider.getBalance(taker);
106
+ const amountWei = BigInt(amount);
107
+ const valueWei = quote.transaction.value ? BigInt(quote.transaction.value) : 0n;
108
+ const totalNeeded = valueWei + gasCostWei;
109
+ if (balance < totalNeeded) {
110
+ const balanceEth = formatAmount(balance.toString());
111
+ const gasEthShort = formatAmount(gasCostWei.toString());
112
+ exitWithError(`Insufficient ETH for swap + gas. Balance: ${balanceEth} ETH. This swap needs ${formatAmount(valueWei.toString())} ETH + ~${gasEthShort} ETH gas. Try a smaller amount (e.g. -a 0.0015) or add more ETH.`, "INSUFFICIENT_FUNDS");
113
+ }
114
+ }
115
+ const sellSym = tokenSymbol(sellToken, chainId);
116
+ const buySym = tokenSymbol(buyToken, chainId);
117
+ const chainName = chainDisplayName(chainId);
118
+ const sellFormatted = formatAmount(amount);
119
+ const buyFormatted = formatAmount(quote.buyAmount);
120
+ if (opts.dryRun) {
121
+ if (isJsonMode()) {
122
+ out({ sellAmount: amount, buyAmount: quote.buyAmount, gas: quote.gas, needsAllowance: !!quote.issues?.allowance?.spender });
123
+ }
124
+ else {
125
+ out(`Swap ${sellFormatted} ${sellSym} → ~${buyFormatted} ${buySym} on ${chainName}`);
126
+ out(`Gas estimate: ${gasEth} ETH (~$${gasUsd})`);
127
+ if (quote.issues?.allowance?.spender)
128
+ out("(Would approve spender first, then swap.)");
129
+ out("Re-run without --dry-run to execute.");
130
+ }
131
+ return;
132
+ }
133
+ if (!yes && !isJsonMode()) {
134
+ out(`Swap ${sellFormatted} ${sellSym} → ~${buyFormatted} ${buySym} on ${chainName}`);
135
+ out(`Estimated gas: ${gasEth} ETH (~$${gasUsd})`);
136
+ if (quote.issues?.allowance?.spender)
137
+ out("(Will approve spender, then swap.)");
138
+ const ok = await askConfirm("Proceed? [y/N] ");
139
+ if (!ok) {
140
+ out("Aborted.");
141
+ process.exit(0);
142
+ }
143
+ }
144
+ const execSpinner = isJsonMode() ? null : ora("Executing swap...").start();
145
+ const txHash = await executeSwapWithQuote(signer, sellToken, quote, amount);
146
+ if (execSpinner)
147
+ execSpinner.succeed("Swap confirmed");
148
+ try {
149
+ const isSellingNative = sellToken.toLowerCase() === NATIVE_ETH_0X.toLowerCase();
150
+ const amountNum = parseFloat(ethers.formatEther(amount));
151
+ const usd = isSellingNative
152
+ ? amountNum * (await getEthUsdPriceNumber(chainId))
153
+ : amountNum * (await getSpeedUsdPriceNumber(chainId));
154
+ recordXpAction("swap", usd);
155
+ }
156
+ catch (_) { }
157
+ const explorer = EXPLORER_URLS[chainId];
158
+ const link = explorer ? `${explorer}/tx/${txHash}` : txHash;
159
+ if (isJsonMode()) {
160
+ out({ txHash, explorerLink: link });
161
+ }
162
+ else {
163
+ success(`Tx: ${txHash}`);
164
+ if (explorer)
165
+ out(link);
166
+ }
167
+ }
168
+ catch (e) {
169
+ const err = e;
170
+ if (err?.code === "INSUFFICIENT_FUNDS" || (typeof err?.message === "string" && err.message.includes("insufficient funds"))) {
171
+ exitWithError("Insufficient balance for swap + gas. When selling native ETH, leave some for gas (e.g. use -a 0.0015 instead of 0.002)." + usageHint("swap"), "INSUFFICIENT_FUNDS");
172
+ }
173
+ const msg = e instanceof Error ? e.message : String(e);
174
+ exitWithError(`${msg}.${usageHint("swap")}`, "SWAP_ERROR");
175
+ }
176
+ });
177
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function volumeCmd(): Command;
@@ -0,0 +1,241 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import ora from "ora";
4
+ import { getSigner } from "../wallet.js";
5
+ import { executeSwap } from "../lib/swap-execute.js";
6
+ import { getEthUsdPriceNumber } from "../lib/oracle.js";
7
+ import { recordXpAction } from "../lib/xp.js";
8
+ import { parseTokenAmountToWei } from "../lib/parse-amount.js";
9
+ import { resolveChainId, getChainOptionsHint, resolveTokenAddress, NATIVE_ETH_0X, EXPLORER_URLS, } from "../constants.js";
10
+ import { getDefaultChainInput } from "./config.js";
11
+ import { out, exitWithError, setJsonMode, isJsonMode, success, usageHint } from "../output.js";
12
+ const ERC20_BALANCE_ABI = ["function balanceOf(address owner) view returns (uint256)"];
13
+ /** Minimum ETH (wei) per buy so 0x doesn't get dust or revert. 0.0001 ETH */
14
+ const MIN_BUY_WEI = 10n ** 14n;
15
+ /** Minimum SPEED (18 decimals) to sell so 0x/DEX doesn't revert. 0.001 SPEED */
16
+ const MIN_SELL_SPEED_WEI = 10n ** 15n;
17
+ function clampBigInt(value, min, max) {
18
+ if (value < min)
19
+ return min;
20
+ if (value > max)
21
+ return max;
22
+ return value;
23
+ }
24
+ /** Random walk step: currentWei * (1 + uniform(-drift, +drift)), clamped to [minWei, maxWei], never below MIN_BUY_WEI. */
25
+ function nextAmountWei(currentWei, drift, minWei, maxWei) {
26
+ const multiplier = 1 + (2 * Math.random() - 1) * drift;
27
+ const scaled = (currentWei * BigInt(Math.round(1000 * multiplier))) / 1000n;
28
+ const clamped = clampBigInt(scaled, minWei, maxWei);
29
+ return clamped < MIN_BUY_WEI ? MIN_BUY_WEI : clamped;
30
+ }
31
+ function jitteredDelaySeconds(centerSec, jitterFraction) {
32
+ const jitter = (2 * Math.random() - 1) * jitterFraction;
33
+ const sec = centerSec * (1 + jitter);
34
+ return Math.max(0, sec);
35
+ }
36
+ function sleepMs(ms) {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+ export function volumeCmd() {
40
+ return new Command("volume")
41
+ .description("Human-like volume: interleaved buys and sells (native ETH ↔ token). Default token is Speed; use --token for others.")
42
+ .option("-c, --chain <id|name>", "Chain ID or name", "8453")
43
+ .option("-t, --token <address|speed>", "Token to buy/sell (default: Speed)", "")
44
+ .option("--ops <n>", "Total number of operations (buy or sell)", "20")
45
+ .option("-a, --amount <amount>", "Initial ETH per buy in token units (e.g. 0.001, 0.1)", "")
46
+ .option("--amount-min <amount>", "Min ETH per buy (e.g. 0.0005)", "")
47
+ .option("--amount-max <amount>", "Max ETH per buy (e.g. 0.002)", "")
48
+ .option("--amount-drift <fraction>", "Max relative step per buy (e.g. 0.1 = ±10%)", "0.1")
49
+ .option("--sell-frequency <0..1>", "Probability next op is a sell (e.g. 0.2 = 1 in 5)", "0.2")
50
+ .option("--sell-partial-chance <0..1>", "When selling, probability of selling partial balance", "0.3")
51
+ .option("--delay <seconds>", "Center of delay between ops (seconds)", "0")
52
+ .option("--delay-jitter <fraction>", "Variance around delay (e.g. 0.5 = ±50%)", "0.5")
53
+ .option("--dry-run", "Do not send any transactions")
54
+ .action(async function (opts) {
55
+ setJsonMode(this.parent?.opts().json ?? false);
56
+ const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
57
+ if (chainId === null) {
58
+ exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}.${usageHint("volume")}`, "INVALID_CHAIN");
59
+ }
60
+ const ops = parseInt(String(opts.ops ?? "20"), 10);
61
+ if (!Number.isInteger(ops) || ops < 1) {
62
+ exitWithError("--ops must be an integer >= 1." + usageHint("volume"), "INVALID_OPS");
63
+ }
64
+ if (!opts.amount?.trim()) {
65
+ exitWithError("--amount is required (initial ETH per buy). Example: speed volume -c base -a 0.001 --ops 20", "MISSING_ARGS");
66
+ }
67
+ const tokenAddress = resolveTokenAddress(opts.token);
68
+ const amountWei = parseTokenAmountToWei(opts.amount.trim());
69
+ let amountMinWei;
70
+ let amountMaxWei;
71
+ if (opts.amountMin?.trim() && opts.amountMax?.trim()) {
72
+ amountMinWei = BigInt(parseTokenAmountToWei(opts.amountMin.trim()));
73
+ amountMaxWei = BigInt(parseTokenAmountToWei(opts.amountMax.trim()));
74
+ }
75
+ else {
76
+ const initial = BigInt(amountWei);
77
+ amountMinWei = (initial * 50n) / 100n;
78
+ amountMaxWei = (initial * 150n) / 100n;
79
+ }
80
+ if (amountMinWei >= amountMaxWei) {
81
+ exitWithError("--amount-min must be less than --amount-max." + usageHint("volume"), "INVALID_AMOUNT");
82
+ }
83
+ if (amountMinWei < MIN_BUY_WEI) {
84
+ amountMinWei = MIN_BUY_WEI;
85
+ }
86
+ if (amountMaxWei < MIN_BUY_WEI) {
87
+ exitWithError(`Amount bounds must be at least ${ethers.formatEther(MIN_BUY_WEI.toString())} ETH.` + usageHint("volume"), "INVALID_AMOUNT");
88
+ }
89
+ const amountDrift = parseFloat(opts.amountDrift ?? "0.1");
90
+ const sellFrequency = parseFloat(opts.sellFrequency ?? "0.2");
91
+ const sellPartialChance = parseFloat(opts.sellPartialChance ?? "0.3");
92
+ const delaySec = parseFloat(opts.delay ?? "0");
93
+ const delayJitter = parseFloat(opts.delayJitter ?? "0.5");
94
+ if (opts.dryRun) {
95
+ if (isJsonMode()) {
96
+ out({
97
+ dryRun: true,
98
+ chainId,
99
+ token: tokenAddress,
100
+ ops,
101
+ amountWei,
102
+ amountMinWei: amountMinWei.toString(),
103
+ amountMaxWei: amountMaxWei.toString(),
104
+ amountDrift,
105
+ sellFrequency,
106
+ sellPartialChance,
107
+ delaySec,
108
+ delayJitter,
109
+ });
110
+ }
111
+ else {
112
+ out(`Dry run: ${ops} ops (buys + sells), initial amount ${ethers.formatEther(amountWei)} ETH`);
113
+ out(`Amount walk: [${ethers.formatEther(amountMinWei.toString())}, ${ethers.formatEther(amountMaxWei.toString())}] ETH, drift ±${amountDrift * 100}%`);
114
+ out(`Sell frequency: ${sellFrequency * 100}%, partial sell chance: ${sellPartialChance * 100}%`);
115
+ out(`Delay: ${delaySec}s ± ${delayJitter * 100}% jitter`);
116
+ out("Re-run without --dry-run to execute.");
117
+ }
118
+ return;
119
+ }
120
+ const signer = getSigner(chainId);
121
+ const taker = await signer.getAddress();
122
+ const tokenContract = new ethers.Contract(tokenAddress, ERC20_BALANCE_ABI, signer);
123
+ let ethUsd = 0;
124
+ try {
125
+ ethUsd = await getEthUsdPriceNumber(chainId);
126
+ }
127
+ catch (_) { }
128
+ let currentAmountWei = BigInt(amountWei);
129
+ const results = [];
130
+ const failures = [];
131
+ let buysDone = 0;
132
+ let sellsDone = 0;
133
+ for (let op = 1; op <= ops; op++) {
134
+ const isSell = Math.random() < sellFrequency;
135
+ if (isSell) {
136
+ const balance = await tokenContract.balanceOf(taker);
137
+ if (balance === 0n || balance < MIN_SELL_SPEED_WEI) {
138
+ if (!isJsonMode())
139
+ out(`Op ${op}/${ops} sell skipped (token balance below minimum ${ethers.formatUnits(MIN_SELL_SPEED_WEI, 18)})`);
140
+ if (op < ops)
141
+ await sleepMs(jitteredDelaySeconds(delaySec, delayJitter) * 1000);
142
+ continue;
143
+ }
144
+ const doPartial = Math.random() < sellPartialChance;
145
+ let sellAmountWei = doPartial
146
+ ? (balance * BigInt(Math.floor(200 + Math.random() * 600))) /* [0.2, 0.8] * 1000 */ / 1000n
147
+ : balance;
148
+ if (sellAmountWei < MIN_SELL_SPEED_WEI) {
149
+ sellAmountWei = MIN_SELL_SPEED_WEI;
150
+ }
151
+ if (sellAmountWei > balance) {
152
+ sellAmountWei = balance;
153
+ }
154
+ if (sellAmountWei <= 0n || sellAmountWei < MIN_SELL_SPEED_WEI) {
155
+ if (op < ops)
156
+ await sleepMs(jitteredDelaySeconds(delaySec, delayJitter) * 1000);
157
+ continue;
158
+ }
159
+ const spinner = isJsonMode() ? null : ora(`Op ${op}/${ops} sell ${doPartial ? "partial" : "all"}...`).start();
160
+ try {
161
+ const { txHash, quote } = await executeSwap(chainId, signer, tokenAddress, NATIVE_ETH_0X, sellAmountWei.toString());
162
+ if (spinner)
163
+ spinner.succeed(`Op ${op}/${ops} sell confirmed`);
164
+ results.push({ op, phase: "sell", txHash, amountWei: sellAmountWei.toString(), partial: doPartial });
165
+ sellsDone++;
166
+ try {
167
+ // Use executed swap value: ETH received (quote.buyAmount) × ETH price
168
+ const ethReceived = parseFloat(ethers.formatEther(quote.buyAmount));
169
+ const usd = ethReceived * ethUsd;
170
+ recordXpAction("volumeOp", usd);
171
+ }
172
+ catch (_) { }
173
+ }
174
+ catch (e) {
175
+ if (spinner)
176
+ spinner.fail(`Op ${op}/${ops} sell failed`);
177
+ const msg = e instanceof Error ? e.message : String(e);
178
+ failures.push({ op, phase: "sell", error: msg });
179
+ if (!isJsonMode())
180
+ out(` → ${msg.slice(0, 80)}${msg.length > 80 ? "…" : ""}`);
181
+ }
182
+ }
183
+ else {
184
+ const buyAmountWei = currentAmountWei < MIN_BUY_WEI ? MIN_BUY_WEI : currentAmountWei;
185
+ const spinner = isJsonMode() ? null : ora(`Op ${op}/${ops} buy...`).start();
186
+ try {
187
+ const { txHash, quote } = await executeSwap(chainId, signer, NATIVE_ETH_0X, tokenAddress, buyAmountWei.toString());
188
+ if (spinner)
189
+ spinner.succeed(`Op ${op}/${ops} buy confirmed`);
190
+ results.push({ op, phase: "buy", txHash, amountWei: buyAmountWei.toString() });
191
+ buysDone++;
192
+ currentAmountWei = nextAmountWei(buyAmountWei, amountDrift, amountMinWei, amountMaxWei);
193
+ try {
194
+ // Use executed swap value: ETH sold (quote.sellAmount) × ETH price
195
+ const ethSold = parseFloat(ethers.formatEther(quote.sellAmount));
196
+ const usd = ethSold * ethUsd;
197
+ recordXpAction("volumeOp", usd);
198
+ }
199
+ catch (_) { }
200
+ }
201
+ catch (e) {
202
+ if (spinner)
203
+ spinner.fail(`Op ${op}/${ops} buy failed`);
204
+ const msg = e instanceof Error ? e.message : String(e);
205
+ failures.push({ op, phase: "buy", error: msg });
206
+ if (!isJsonMode())
207
+ out(` → ${msg.slice(0, 80)}${msg.length > 80 ? "…" : ""}`);
208
+ }
209
+ }
210
+ if (op < ops) {
211
+ const waitSec = jitteredDelaySeconds(delaySec, delayJitter);
212
+ if (waitSec > 0)
213
+ await sleepMs(waitSec * 1000);
214
+ }
215
+ }
216
+ const explorer = EXPLORER_URLS[chainId];
217
+ const failedCount = failures.length;
218
+ if (isJsonMode()) {
219
+ out({
220
+ summary: { buys: buysDone, sells: sellsDone, failed: failedCount, totalOps: ops },
221
+ results: results.map((r) => ({
222
+ ...r,
223
+ explorerLink: r.txHash && explorer ? `${explorer}/tx/${r.txHash}` : r.txHash,
224
+ })),
225
+ failures: failures.length ? failures : undefined,
226
+ });
227
+ }
228
+ else {
229
+ success(`Volume complete: ${buysDone} buys, ${sellsDone} sells${failedCount ? `, ${failedCount} failed` : ""} (${ops} ops)`);
230
+ results.forEach((r) => {
231
+ if (r.txHash) {
232
+ const link = explorer ? `${explorer}/tx/${r.txHash}` : r.txHash;
233
+ out(` ${r.phase} op ${r.op}: ${link}`);
234
+ }
235
+ });
236
+ if (failures.length) {
237
+ out(`Failed: ${failures.map((f) => `op ${f.op} (${f.phase})`).join(", ")}`);
238
+ }
239
+ }
240
+ });
241
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function whoamiCmd(): Command;
@@ -0,0 +1,23 @@
1
+ import { Command } from "commander";
2
+ import { getAddress } from "../wallet.js";
3
+ import { out, exitWithError, isJsonMode, setJsonMode } from "../output.js";
4
+ export function whoamiCmd() {
5
+ return new Command("whoami")
6
+ .description("Print address derived from PRIVATE_KEY (sanity check; no tx)")
7
+ .action(async function () {
8
+ setJsonMode(this.parent?.opts().json ?? false);
9
+ try {
10
+ const address = getAddress();
11
+ if (isJsonMode()) {
12
+ out({ address });
13
+ }
14
+ else {
15
+ out(address);
16
+ }
17
+ }
18
+ catch (e) {
19
+ const msg = e instanceof Error ? e.message : String(e);
20
+ exitWithError(`${msg} Set PRIVATE_KEY in env or run: speed setup`, "PRIVATE_KEY_MISSING");
21
+ }
22
+ });
23
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function xpCmd(): Command;
@@ -0,0 +1,56 @@
1
+ import { Command } from "commander";
2
+ import { loadXpState, resetXpState, getTitleForLevel, getProgressInLevel, getXpForNextLevel, } from "../lib/xp.js";
3
+ import { out, setJsonMode, isJsonMode, success } from "../output.js";
4
+ export function xpCmd() {
5
+ return new Command("xp")
6
+ .description("Show XP progress: level, streak, stats. Bots use this to see how they're doing.")
7
+ .option("--no-title", "Omit the silly level title")
8
+ .option("--reset", "Clear all XP and stats (back to level 1)")
9
+ .action(async function (opts) {
10
+ setJsonMode(this.parent?.opts().json ?? false);
11
+ if (opts.reset) {
12
+ resetXpState();
13
+ if (isJsonMode()) {
14
+ out({ reset: true });
15
+ }
16
+ else {
17
+ success("XP cleared. You're back to level 1.");
18
+ }
19
+ return;
20
+ }
21
+ const state = loadXpState();
22
+ const level = state.level;
23
+ const progress = getProgressInLevel(state.totalXP);
24
+ const title = getTitleForLevel(level);
25
+ const nextXp = getXpForNextLevel(level);
26
+ if (isJsonMode()) {
27
+ out({
28
+ totalXP: state.totalXP,
29
+ level: state.level,
30
+ title: opts.title !== false ? title : undefined,
31
+ streak: state.streak,
32
+ lastActivity: state.lastActivity || undefined,
33
+ progress: { current: progress.current, needed: progress.needed, fraction: progress.fraction },
34
+ stats: state.stats,
35
+ recentHistory: state.history.slice(-10),
36
+ });
37
+ return;
38
+ }
39
+ if (opts.title !== false) {
40
+ out(`\n ╭─────────────────────────────╮`);
41
+ out(` │ Level ${String(level).padStart(3)} ${title.padEnd(18)} │`);
42
+ out(` ╰─────────────────────────────╯\n`);
43
+ }
44
+ out(` Total XP: ${state.totalXP.toLocaleString()}`);
45
+ out(` Progress: ${progress.current} / ${nextXp} XP to level ${level + 1} (${(progress.fraction * 100).toFixed(0)}%)`);
46
+ out(` Streak: ${state.streak} day(s) │ Last activity: ${state.lastActivity || "—"}`);
47
+ out("");
48
+ out(" Stats:");
49
+ out(` Swaps: ${state.stats.swaps.count} ($${state.stats.swaps.totalUSD.toFixed(0)} total)`);
50
+ out(` Bridges: ${state.stats.bridges.count} ($${state.stats.bridges.totalUSD.toFixed(0)} total)`);
51
+ out(` Volume ops: ${state.stats.volumeOps.count} ($${state.stats.volumeOps.totalUSD.toFixed(0)} total)`);
52
+ out(` DCA buys: ${state.stats.dcaBuys.count} ($${state.stats.dcaBuys.totalUSD.toFixed(0)} total)`);
53
+ out(` Gas refuel: ${state.stats.gasRefuels.count} ($${state.stats.gasRefuels.totalUSD.toFixed(0)} total)`);
54
+ out("");
55
+ });
56
+ }
@@ -0,0 +1,66 @@
1
+ /** Speed Token address (same on supported chains where applicable) */
2
+ export declare const SPEED_TOKEN_ADDRESS = "0xB01CF1bE9568f09449382a47Cd5bF58e2A9D5922";
3
+ /** 0x API uses this address for native ETH (sell-token in quotes) */
4
+ export declare const NATIVE_ETH_0X = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
5
+ /** Supported chain IDs: Ethereum, Base, OP, Arbitrum, Polygon, BNB */
6
+ export declare const SUPPORTED_CHAIN_IDS: readonly [1, 8453, 10, 42161, 137, 56];
7
+ export type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number];
8
+ /** Chain ID to primary display name */
9
+ export declare const CHAIN_NAMES: Record<number, string>;
10
+ /** Name/alias (lowercase) to chain ID. Use resolveChainId() for parsing. */
11
+ export declare const CHAIN_NAME_TO_ID: Record<string, number>;
12
+ /**
13
+ * Resolve chain from ID number or name (e.g. "8453", "base", "ethereum").
14
+ * Returns the chain ID if supported, otherwise null.
15
+ */
16
+ export declare function resolveChainId(input: string): SupportedChainId | null;
17
+ /** Resolve token option: empty or "speed" → SPEED_TOKEN_ADDRESS, else return trimmed address. */
18
+ export declare function resolveTokenAddress(input: string | undefined): string;
19
+ /** Human-readable list of supported chains for error messages (e.g. "1 (ethereum), 8453 (base), ...") */
20
+ export declare function getChainOptionsHint(): string;
21
+ /** Alchemy chain prefix for RPC URL */
22
+ export declare const ALCHEMY_CHAIN_PREFIX: Record<number, string>;
23
+ /** One public fallback RPC per chain when Alchemy is not set */
24
+ export declare const PUBLIC_RPC_FALLBACKS: Record<number, string>;
25
+ /** Lightspeed price oracle addresses by chain (same ABI on all) */
26
+ export declare const ORACLE_ADDRESSES: Record<number, string>;
27
+ /** Minimal ABI for oracle reads: getLatestSpeedEthPrice, getLatestSpeedUsdPrice, getEthUsdPrice */
28
+ export declare const ORACLE_ABI: readonly [{
29
+ readonly inputs: readonly [];
30
+ readonly name: "getLatestSpeedEthPrice";
31
+ readonly outputs: readonly [{
32
+ readonly internalType: "uint256";
33
+ readonly name: "";
34
+ readonly type: "uint256";
35
+ }];
36
+ readonly stateMutability: "view";
37
+ readonly type: "function";
38
+ }, {
39
+ readonly inputs: readonly [];
40
+ readonly name: "getLatestSpeedUsdPrice";
41
+ readonly outputs: readonly [{
42
+ readonly internalType: "uint256";
43
+ readonly name: "";
44
+ readonly type: "uint256";
45
+ }];
46
+ readonly stateMutability: "view";
47
+ readonly type: "function";
48
+ }, {
49
+ readonly inputs: readonly [];
50
+ readonly name: "getEthUsdPrice";
51
+ readonly outputs: readonly [{
52
+ readonly internalType: "uint256";
53
+ readonly name: "";
54
+ readonly type: "uint256";
55
+ }];
56
+ readonly stateMutability: "view";
57
+ readonly type: "function";
58
+ }];
59
+ export declare const ZEROX_API_BASE = "https://api.0x.org";
60
+ export declare const SQUID_BASE_URL = "https://v2.api.squidrouter.com";
61
+ /** Native token symbol per chain (for balance display) */
62
+ export declare const NATIVE_SYMBOL: Record<number, string>;
63
+ /** Block explorer base URLs by chain for tx links */
64
+ export declare const EXPLORER_URLS: Record<number, string>;
65
+ /** Wrapped native token address per chain (for gas command: swap Speed → native) */
66
+ export declare const WRAPPED_NATIVE_ADDRESS: Record<number, string>;