@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
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # speed-cli
2
+
3
+ CLI for Speed Token: swap (0x), bridge (Squid), balance, price, volume, DCA, gas, XP. Config and secrets in `~/.speed`.
4
+
5
+ ## Install (use the `speed` command)
6
+
7
+ **Global install (recommended):**
8
+ ```bash
9
+ npm install -g @lightspeed-cli/speed-cli
10
+ speed whoami
11
+ speed balance --json
12
+ ```
13
+
14
+ **Or run without installing:**
15
+ ```bash
16
+ npx @lightspeed-cli/speed-cli whoami
17
+ npx @lightspeed-cli/speed-cli balance --json
18
+ ```
19
+
20
+ **First-time setup:** Run `speed setup` to configure `~/.speed/.env` (private key, 0x API key, etc.). See `.env.example` for required vars.
21
+
22
+ ## Publish this package to npm
23
+
24
+ 1. **Log in to npm** (one-time):
25
+ ```bash
26
+ npm login
27
+ ```
28
+ Use your npm account; create one at [npmjs.com](https://www.npmjs.com/signup) if needed.
29
+
30
+ 2. **Build and publish** (scoped packages require `--access public`):
31
+ ```bash
32
+ npm run build
33
+ npm publish --access public
34
+ ```
35
+
36
+ 3. **After publishing**, anyone can install and use the `speed` command:
37
+ ```bash
38
+ npm install -g @lightspeed-cli/speed-cli
39
+ speed --help
40
+ ```
41
+
42
+ ## Develop locally
43
+
44
+ ```bash
45
+ npm install
46
+ npm run build
47
+ node dist/cli.js --help
48
+ # or link globally to test the "speed" command:
49
+ npm link
50
+ speed whoami
51
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { loadEnv } from "./env.js";
3
+ loadEnv();
4
+ import { Command } from "commander";
5
+ import { whoamiCmd } from "./commands/whoami.js";
6
+ import { balanceCmd } from "./commands/balance.js";
7
+ import { quoteCmd } from "./commands/quote.js";
8
+ import { swapCmd } from "./commands/swap.js";
9
+ import { volumeCmd } from "./commands/volume.js";
10
+ import { dcaCmd } from "./commands/dca.js";
11
+ import { revokeCmd } from "./commands/revoke.js";
12
+ import { bridgeCmd } from "./commands/bridge.js";
13
+ import { statusCmd } from "./commands/status.js";
14
+ import { estimateCmd } from "./commands/estimate.js";
15
+ import { priceCmd } from "./commands/price.js";
16
+ import { doctorCmd } from "./commands/doctor.js";
17
+ import { configCmd } from "./commands/config.js";
18
+ import { sendCmd } from "./commands/send.js";
19
+ import { allowanceCmd } from "./commands/allowance.js";
20
+ import { approveCmd } from "./commands/approve.js";
21
+ import { gasCmd } from "./commands/gas.js";
22
+ import { historyCmd } from "./commands/history.js";
23
+ import { pendingCmd } from "./commands/pending.js";
24
+ import { setupCmd } from "./commands/setup.js";
25
+ import { xpCmd } from "./commands/xp.js";
26
+ const program = new Command();
27
+ program
28
+ .name("speed")
29
+ .description("Speed Token CLI: swap, bridge, balance, price, volume, dca, gas, history, xp, and more. Default token is Speed.")
30
+ .version("0.1.0")
31
+ .option("-y, --yes", "Skip confirmation for swap/bridge (safe for scripts)")
32
+ .option("--json", "Output machine-readable JSON to stdout (for scripts and OpenClaw)");
33
+ program.addCommand(whoamiCmd());
34
+ program.addCommand(setupCmd());
35
+ program.addCommand(doctorCmd());
36
+ program.addCommand(balanceCmd());
37
+ program.addCommand(quoteCmd());
38
+ program.addCommand(swapCmd());
39
+ program.addCommand(volumeCmd());
40
+ program.addCommand(dcaCmd());
41
+ program.addCommand(revokeCmd());
42
+ program.addCommand(bridgeCmd());
43
+ program.addCommand(statusCmd());
44
+ program.addCommand(estimateCmd());
45
+ program.addCommand(priceCmd());
46
+ program.addCommand(sendCmd());
47
+ program.addCommand(allowanceCmd());
48
+ program.addCommand(approveCmd());
49
+ program.addCommand(gasCmd());
50
+ program.addCommand(historyCmd());
51
+ program.addCommand(pendingCmd());
52
+ program.addCommand(xpCmd());
53
+ program.addCommand(configCmd());
54
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function allowanceCmd(): Command;
@@ -0,0 +1,37 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import { getSigner } from "../wallet.js";
4
+ import { resolveChainId, getChainOptionsHint } from "../constants.js";
5
+ import { getDefaultChainInput } from "./config.js";
6
+ import { out, exitWithError, setJsonMode, isJsonMode, usageHint } from "../output.js";
7
+ const ERC20_ABI = ["function allowance(address owner, address spender) view returns (uint256)"];
8
+ export function allowanceCmd() {
9
+ return new Command("allowance")
10
+ .description("Check current allowance for a spender (0x AllowanceHolder or Squid)")
11
+ .option("-c, --chain <id|name>", "Chain ID or name", "8453")
12
+ .requiredOption("--token <address>", "Token contract address")
13
+ .requiredOption("--spender <address>", "Spender address")
14
+ .action(async function (opts) {
15
+ setJsonMode(this.parent?.opts().json ?? false);
16
+ const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
17
+ if (chainId === null) {
18
+ exitWithError(`Unknown or unsupported chain: ${opts.chain}. Use: ${getChainOptionsHint()}`, "INVALID_CHAIN");
19
+ }
20
+ try {
21
+ const signer = getSigner(chainId);
22
+ const owner = await signer.getAddress();
23
+ const token = new ethers.Contract(opts.token.trim(), ERC20_ABI, signer.provider);
24
+ const raw = await token.allowance(owner, opts.spender.trim());
25
+ if (isJsonMode()) {
26
+ out({ allowance: raw.toString(), owner, spender: opts.spender, token: opts.token });
27
+ }
28
+ else {
29
+ out(`Allowance: ${raw.toString()}`);
30
+ }
31
+ }
32
+ catch (e) {
33
+ const msg = e instanceof Error ? e.message : String(e);
34
+ exitWithError(`${msg}.${usageHint("allowance")}`, "ALLOWANCE_ERROR");
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function approveCmd(): Command;
@@ -0,0 +1,41 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import { getSigner } from "../wallet.js";
4
+ import { parseTokenAmountToWei } from "../lib/parse-amount.js";
5
+ import { resolveChainId, getChainOptionsHint } from "../constants.js";
6
+ import { getDefaultChainInput } from "./config.js";
7
+ import { out, exitWithError, setJsonMode, isJsonMode, success, usageHint } from "../output.js";
8
+ const ERC20_ABI = ["function approve(address spender, uint256 amount) returns (bool)"];
9
+ export function approveCmd() {
10
+ return new Command("approve")
11
+ .description("Manually set allowance for a spender (for scripting)")
12
+ .option("-c, --chain <id|name>", "Chain ID or name", "8453")
13
+ .requiredOption("--token <address>", "Token contract address")
14
+ .requiredOption("--spender <address>", "Spender address")
15
+ .requiredOption("-a, --amount <amount|max>", "Amount in token units or 'max' for unlimited")
16
+ .action(async function (opts) {
17
+ setJsonMode(this.parent?.opts().json ?? false);
18
+ const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
19
+ if (chainId === null) {
20
+ exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}. Example: speed approve -c base --token <addr> --spender <addr> -a <amount|max>`, "INVALID_CHAIN");
21
+ }
22
+ const amountRaw = opts.amount.trim();
23
+ const amount = amountRaw.toLowerCase() === "max" ? ethers.MaxUint256 : BigInt(parseTokenAmountToWei(amountRaw));
24
+ try {
25
+ const signer = getSigner(chainId);
26
+ const token = new ethers.Contract(opts.token.trim(), ERC20_ABI, signer);
27
+ const tx = await token.approve(opts.spender.trim(), amount);
28
+ await tx.wait();
29
+ if (isJsonMode()) {
30
+ out({ txHash: tx.hash, token: opts.token, spender: opts.spender });
31
+ }
32
+ else {
33
+ success(`Approved. Tx: ${tx.hash}`);
34
+ }
35
+ }
36
+ catch (e) {
37
+ const msg = e instanceof Error ? e.message : String(e);
38
+ exitWithError(`${msg}.${usageHint("approve")}`, "APPROVE_ERROR");
39
+ }
40
+ });
41
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function balanceCmd(): Command;
@@ -0,0 +1,100 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import { getAddress } from "../wallet.js";
4
+ import { getRpcUrl } from "../rpc.js";
5
+ import { SPEED_TOKEN_ADDRESS, SUPPORTED_CHAIN_IDS, NATIVE_SYMBOL, CHAIN_NAMES, resolveChainId, getChainOptionsHint, } from "../constants.js";
6
+ import { out, exitWithError, setJsonMode, isJsonMode, usageHint } from "../output.js";
7
+ import chalk from "chalk";
8
+ const ERC20_ABI = [
9
+ "function balanceOf(address owner) view returns (uint256)",
10
+ "function decimals() view returns (uint8)",
11
+ "function symbol() view returns (string)",
12
+ ];
13
+ async function getBalancesForChain(chainId, address, extraTokenAddresses) {
14
+ const provider = new ethers.JsonRpcProvider(getRpcUrl(chainId));
15
+ const nativeSymbol = NATIVE_SYMBOL[chainId] ?? "ETH";
16
+ const speed = new ethers.Contract(SPEED_TOKEN_ADDRESS, ERC20_ABI, provider);
17
+ const [nativeBal, speedDecimals, speedSymbol, speedBal] = await Promise.all([
18
+ provider.getBalance(address),
19
+ speed.decimals(),
20
+ speed.symbol().catch(() => "SPEED"),
21
+ speed.balanceOf(address),
22
+ ]);
23
+ const tokens = [
24
+ {
25
+ address: SPEED_TOKEN_ADDRESS,
26
+ symbol: speedSymbol,
27
+ balance: ethers.formatUnits(speedBal, speedDecimals),
28
+ decimals: speedDecimals,
29
+ },
30
+ ];
31
+ const extraAddrs = extraTokenAddresses
32
+ .map((a) => a.trim())
33
+ .filter((a) => a && a.toLowerCase() !== SPEED_TOKEN_ADDRESS.toLowerCase());
34
+ const extraResults = await Promise.allSettled(extraAddrs.map(async (a) => {
35
+ const token = new ethers.Contract(a, ERC20_ABI, provider);
36
+ const [dec, sym, bal] = await Promise.all([
37
+ token.decimals(),
38
+ token.symbol().catch(() => "???"),
39
+ token.balanceOf(address),
40
+ ]);
41
+ return { address: a, symbol: sym, balance: ethers.formatUnits(bal, dec), decimals: dec };
42
+ }));
43
+ for (let i = 0; i < extraResults.length; i++) {
44
+ const result = extraResults[i];
45
+ if (result.status === "fulfilled")
46
+ tokens.push(result.value);
47
+ else
48
+ tokens.push({ address: extraAddrs[i], symbol: "???", balance: "0", decimals: 18 });
49
+ }
50
+ return {
51
+ chainId,
52
+ nativeSymbol,
53
+ nativeBalance: ethers.formatEther(nativeBal),
54
+ tokens,
55
+ };
56
+ }
57
+ export function balanceCmd() {
58
+ return new Command("balance")
59
+ .description("Show Speed + native token + optional other tokens. No --chain = all chains; --chain = that chain only.")
60
+ .option("-c, --chain <id|name>", "Chain ID or name (e.g. 8453, base). Omit to show all chains.")
61
+ .option("-t, --token <address>", "Extra token address (can be repeated)", (v, prev) => [...(prev ?? []), v], [])
62
+ .action(async function (opts) {
63
+ setJsonMode(this.parent?.opts().json ?? false);
64
+ const extraTokens = Array.isArray(opts.token) ? opts.token : opts.token ? [opts.token] : [];
65
+ let chainsToQuery = [...SUPPORTED_CHAIN_IDS];
66
+ if (opts.chain !== undefined) {
67
+ const resolved = resolveChainId(opts.chain);
68
+ if (resolved === null) {
69
+ exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}.${usageHint("balance")}`, "INVALID_CHAIN");
70
+ }
71
+ chainsToQuery = [resolved];
72
+ }
73
+ try {
74
+ const address = getAddress();
75
+ const results = await Promise.all(chainsToQuery.map((chainId) => getBalancesForChain(chainId, address, extraTokens)));
76
+ if (isJsonMode()) {
77
+ out({ address, chains: results });
78
+ return;
79
+ }
80
+ const chainCol = 14;
81
+ const numCol = 14;
82
+ out(chalk.bold("Chain".padEnd(chainCol) + "Native".padEnd(numCol) + "Speed".padEnd(numCol) + "Tokens"));
83
+ out("-".repeat(chainCol + numCol * 2 + 8));
84
+ for (const chain of results) {
85
+ const name = (CHAIN_NAMES[chain.chainId] ?? String(chain.chainId)).padEnd(chainCol);
86
+ const native = `${chain.nativeBalance} ${chain.nativeSymbol}`.padEnd(numCol);
87
+ const speed = (chain.tokens.find((t) => t.address.toLowerCase() === SPEED_TOKEN_ADDRESS.toLowerCase())?.balance ?? "0").padEnd(numCol);
88
+ const others = chain.tokens
89
+ .filter((t) => t.address.toLowerCase() !== SPEED_TOKEN_ADDRESS.toLowerCase())
90
+ .map((t) => `${t.symbol}: ${t.balance}`)
91
+ .join(", ");
92
+ out(name + native + speed + (others || "—"));
93
+ }
94
+ }
95
+ catch (e) {
96
+ const msg = e instanceof Error ? e.message : String(e);
97
+ exitWithError(`${msg}.${usageHint("balance")}`, "BALANCE_ERROR");
98
+ }
99
+ });
100
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function bridgeCmd(): Command;
@@ -0,0 +1,142 @@
1
+ import { Command } from "commander";
2
+ import { createInterface } from "readline";
3
+ import ora from "ora";
4
+ import { getSigner } from "../wallet.js";
5
+ import { getSquid } from "../lib/squid.js";
6
+ import { getEthUsdPriceNumber } from "../lib/oracle.js";
7
+ import { ethers } from "ethers";
8
+ import { EXPLORER_URLS, SPEED_TOKEN_ADDRESS, resolveChainId, getChainOptionsHint } from "../constants.js";
9
+ import { getDefaultChainInput } from "./config.js";
10
+ import { out, exitWithError, setJsonMode, isJsonMode, success, usageHint } from "../output.js";
11
+ import { addPendingBridge } from "../lib/pending-bridges.js";
12
+ import { getSpeedUsdPriceNumber } from "../lib/oracle.js";
13
+ import { recordXpAction } from "../lib/xp.js";
14
+ import { parseTokenAmountToWei } from "../lib/parse-amount.js";
15
+ const ERC20_ABI = ["function approve(address spender, uint256 amount) returns (bool)"];
16
+ function askConfirm(prompt) {
17
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18
+ return new Promise((resolve) => {
19
+ rl.question(prompt, (answer) => {
20
+ rl.close();
21
+ resolve(/^y|yes$/i.test(answer.trim()));
22
+ });
23
+ });
24
+ }
25
+ export function bridgeCmd() {
26
+ return new Command("bridge")
27
+ .description("Bridge Speed to Ethereum, Base, OP, Arbitrum, BNB, or Polygon via Squid. Chain can be ID (e.g. 8453) or name (e.g. base, ethereum).")
28
+ .option("--from-chain <id|name>", "Source chain (ID or name, e.g. base or 8453)", "8453")
29
+ .option("--to-chain <id|name>", "Destination chain (ID or name, e.g. ethereum or 1)")
30
+ .option("-a, --amount <amount>", "Amount of SPEED to bridge (e.g. 0.1, 1000)")
31
+ .option("--to-token <address>", "Destination token (default: Speed on to-chain)")
32
+ .action(async function (opts) {
33
+ setJsonMode(this.parent?.opts().json ?? false);
34
+ const yes = this.parent?.opts().yes ?? false;
35
+ const fromChainId = resolveChainId(opts.fromChain ?? getDefaultChainInput());
36
+ const toChainId = opts.toChain ? resolveChainId(opts.toChain) : null;
37
+ if (fromChainId === null) {
38
+ exitWithError(`Unknown from-chain "${opts.fromChain}" (use chain ID or name). Use one of: ${getChainOptionsHint()}.${usageHint("bridge")}`, "INVALID_CHAIN");
39
+ }
40
+ if (toChainId === null) {
41
+ exitWithError(`--to-chain is required (chain ID or name). Use one of: ${getChainOptionsHint()}. Example: speed bridge --from-chain base --to-chain ethereum -a 1000`, "INVALID_CHAIN");
42
+ }
43
+ if (!opts.amount?.trim()) {
44
+ exitWithError("--amount is required. Example: speed bridge --from-chain base --to-chain ethereum -a 1000", "MISSING_ARGS");
45
+ }
46
+ const amount = parseTokenAmountToWei(opts.amount.trim());
47
+ const toToken = opts.toToken?.trim() ?? SPEED_TOKEN_ADDRESS;
48
+ try {
49
+ const signer = getSigner(fromChainId);
50
+ const fromAddress = await signer.getAddress();
51
+ const spinner = isJsonMode() ? null : ora("Initializing Squid...").start();
52
+ const squid = await getSquid();
53
+ if (spinner)
54
+ spinner.text = "Fetching route...";
55
+ const { route, requestId } = await squid.getRoute({
56
+ fromAddress,
57
+ fromChain: String(fromChainId),
58
+ fromToken: SPEED_TOKEN_ADDRESS,
59
+ fromAmount: amount,
60
+ toChain: String(toChainId),
61
+ toToken,
62
+ toAddress: fromAddress,
63
+ });
64
+ if (spinner)
65
+ spinner.succeed("Route received");
66
+ let gasEth = "0";
67
+ let gasUsd = "0";
68
+ try {
69
+ const ethUsd = await getEthUsdPriceNumber(fromChainId);
70
+ const feeData = await signer.provider.getFeeData();
71
+ const gasLimit = 300000n;
72
+ const gasPrice = feeData.gasPrice ?? 0n;
73
+ gasEth = ethers.formatEther(gasLimit * gasPrice);
74
+ gasUsd = (parseFloat(gasEth) * ethUsd).toFixed(2);
75
+ }
76
+ catch (_) { }
77
+ if (!yes && !isJsonMode()) {
78
+ const amountHuman = parseFloat(ethers.formatEther(amount)).toLocaleString(undefined, { maximumFractionDigits: 6 });
79
+ out(`Bridge ${amountHuman} SPEED from chain ${fromChainId} to chain ${toChainId}`);
80
+ out(`Estimated gas: ${gasEth} ETH (~$${gasUsd})`);
81
+ const ok = await askConfirm("Proceed? [y/N] ");
82
+ if (!ok) {
83
+ out("Aborted.");
84
+ process.exit(0);
85
+ }
86
+ }
87
+ const txRequest = route.transactionRequest;
88
+ const target = txRequest?.target;
89
+ if (target) {
90
+ const approveSpinner = isJsonMode() ? null : ora("Approving...").start();
91
+ const token = new ethers.Contract(SPEED_TOKEN_ADDRESS, ERC20_ABI, signer);
92
+ const txApprove = await token.approve(target, amount);
93
+ await txApprove.wait();
94
+ if (approveSpinner)
95
+ approveSpinner.succeed("Approved");
96
+ }
97
+ const execSpinner = isJsonMode() ? null : ora("Executing bridge...").start();
98
+ const txResponse = await squid.executeRoute({ signer: signer, route });
99
+ const txHash = typeof txResponse === "object" && txResponse !== null && "hash" in txResponse
100
+ ? txResponse.hash
101
+ : txResponse?.transactionHash ?? "unknown";
102
+ if (txResponse && typeof txResponse === "object" && "wait" in txResponse && typeof txResponse.wait === "function") {
103
+ await txResponse.wait();
104
+ }
105
+ if (execSpinner)
106
+ execSpinner.succeed("Bridge tx sent");
107
+ const quoteId = route?.params?.quoteId ??
108
+ route?.quoteId ??
109
+ "";
110
+ addPendingBridge({
111
+ requestId,
112
+ quoteId,
113
+ txHash,
114
+ fromChain: fromChainId,
115
+ toChain: toChainId,
116
+ startedAt: new Date().toISOString(),
117
+ });
118
+ try {
119
+ const amountNum = parseFloat(ethers.formatEther(amount));
120
+ const speedUsd = await getSpeedUsdPriceNumber(fromChainId);
121
+ recordXpAction("bridge", amountNum * speedUsd);
122
+ }
123
+ catch (_) { }
124
+ const explorer = EXPLORER_URLS[fromChainId];
125
+ const link = explorer ? `${explorer}/tx/${txHash}` : txHash;
126
+ const axelarScan = `https://axelarscan.io/gmp/${txHash}`;
127
+ if (isJsonMode()) {
128
+ out({ txHash, explorerLink: link, axelarScan, requestId });
129
+ }
130
+ else {
131
+ success(`Tx: ${txHash}`);
132
+ if (explorer)
133
+ out(link);
134
+ out(`Bridge status: ${axelarScan}`);
135
+ }
136
+ }
137
+ catch (e) {
138
+ const msg = e instanceof Error ? e.message : String(e);
139
+ exitWithError(`${msg}.${usageHint("bridge")}`, "BRIDGE_ERROR");
140
+ }
141
+ });
142
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ interface Config {
3
+ defaultChain?: number;
4
+ defaultSlippage?: number;
5
+ outputFormat?: "human" | "json";
6
+ }
7
+ export declare function configCmd(): Command;
8
+ export declare function getConfig(): Config;
9
+ /** Default chain when user omits -c/--chain (from config or 8453). */
10
+ export declare function getDefaultChainInput(): string;
11
+ export {};
@@ -0,0 +1,83 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { out, exitWithError, setJsonMode, isJsonMode } from "../output.js";
5
+ import { getSpeedConfigDir } from "../env.js";
6
+ const CONFIG_PATH = join(getSpeedConfigDir(), "config.json");
7
+ function loadConfig() {
8
+ try {
9
+ if (existsSync(CONFIG_PATH)) {
10
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
11
+ return JSON.parse(raw);
12
+ }
13
+ }
14
+ catch (_) { }
15
+ return {};
16
+ }
17
+ function saveConfig(config) {
18
+ mkdirSync(getSpeedConfigDir(), { recursive: true });
19
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
20
+ }
21
+ export function configCmd() {
22
+ const cmd = new Command("config")
23
+ .description("Read/write preferences in ~/.speed/config.json (no secrets)");
24
+ cmd
25
+ .command("set <key> <value>")
26
+ .description("Set default-chain, default-slippage, or output-format")
27
+ .action(function (key, value) {
28
+ setJsonMode(this.parent?.parent?.opts().json ?? false);
29
+ const config = loadConfig();
30
+ if (key === "default-chain") {
31
+ config.defaultChain = parseInt(value, 10);
32
+ }
33
+ else if (key === "default-slippage") {
34
+ config.defaultSlippage = parseFloat(value);
35
+ }
36
+ else if (key === "output-format") {
37
+ if (value !== "human" && value !== "json") {
38
+ exitWithError("output-format must be 'human' or 'json'. Example: speed config set output-format json", "INVALID_VALUE");
39
+ }
40
+ config.outputFormat = value;
41
+ }
42
+ else {
43
+ exitWithError(`Unknown key: ${key}. Valid keys: default-chain, default-slippage, output-format. Example: speed config set default-chain 8453`, "INVALID_KEY");
44
+ }
45
+ saveConfig(config);
46
+ if (!isJsonMode())
47
+ out(`Set ${key} = ${value}`);
48
+ else
49
+ out({ [key]: value });
50
+ });
51
+ cmd
52
+ .command("get [key]")
53
+ .description("Get one key or all config")
54
+ .action(function (key) {
55
+ setJsonMode(this.parent?.parent?.opts().json ?? false);
56
+ const config = loadConfig();
57
+ if (key) {
58
+ const v = key === "default-chain" ? config.defaultChain : key === "default-slippage" ? config.defaultSlippage : key === "output-format" ? config.outputFormat : undefined;
59
+ if (v === undefined)
60
+ exitWithError(`Unknown key: ${key}. Valid keys: default-chain, default-slippage, output-format. Example: speed config get default-chain`, "INVALID_KEY");
61
+ if (isJsonMode())
62
+ out({ [key]: v });
63
+ else
64
+ out(String(v));
65
+ }
66
+ else {
67
+ if (isJsonMode())
68
+ out(config);
69
+ else
70
+ for (const [k, v] of Object.entries(config))
71
+ out(`${k}: ${v}`);
72
+ }
73
+ });
74
+ return cmd;
75
+ }
76
+ export function getConfig() {
77
+ return loadConfig();
78
+ }
79
+ /** Default chain when user omits -c/--chain (from config or 8453). */
80
+ export function getDefaultChainInput() {
81
+ const c = loadConfig().defaultChain;
82
+ return c != null ? String(c) : "8453";
83
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function dcaCmd(): Command;