@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.
- package/README.md +51 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +54 -0
- package/dist/commands/allowance.d.ts +2 -0
- package/dist/commands/allowance.js +37 -0
- package/dist/commands/approve.d.ts +2 -0
- package/dist/commands/approve.js +41 -0
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +100 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +142 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +83 -0
- package/dist/commands/dca.d.ts +2 -0
- package/dist/commands/dca.js +175 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +113 -0
- package/dist/commands/estimate.d.ts +2 -0
- package/dist/commands/estimate.js +57 -0
- package/dist/commands/gas.d.ts +2 -0
- package/dist/commands/gas.js +128 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +91 -0
- package/dist/commands/pending.d.ts +2 -0
- package/dist/commands/pending.js +125 -0
- package/dist/commands/price.d.ts +2 -0
- package/dist/commands/price.js +35 -0
- package/dist/commands/quote.d.ts +2 -0
- package/dist/commands/quote.js +54 -0
- package/dist/commands/revoke.d.ts +2 -0
- package/dist/commands/revoke.js +38 -0
- package/dist/commands/send.d.ts +2 -0
- package/dist/commands/send.js +43 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +104 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +64 -0
- package/dist/commands/swap.d.ts +2 -0
- package/dist/commands/swap.js +177 -0
- package/dist/commands/volume.d.ts +2 -0
- package/dist/commands/volume.js +241 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +23 -0
- package/dist/commands/xp.d.ts +2 -0
- package/dist/commands/xp.js +56 -0
- package/dist/constants.d.ts +66 -0
- package/dist/constants.js +135 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +58 -0
- package/dist/lib/alchemy-history.d.ts +24 -0
- package/dist/lib/alchemy-history.js +75 -0
- package/dist/lib/explorer.d.ts +24 -0
- package/dist/lib/explorer.js +59 -0
- package/dist/lib/oracle.d.ts +22 -0
- package/dist/lib/oracle.js +70 -0
- package/dist/lib/parse-amount.d.ts +8 -0
- package/dist/lib/parse-amount.js +19 -0
- package/dist/lib/pending-bridges.d.ts +12 -0
- package/dist/lib/pending-bridges.js +34 -0
- package/dist/lib/squid.d.ts +28 -0
- package/dist/lib/squid.js +23 -0
- package/dist/lib/swap-execute.d.ts +16 -0
- package/dist/lib/swap-execute.js +49 -0
- package/dist/lib/xp.d.ts +57 -0
- package/dist/lib/xp.js +217 -0
- package/dist/lib/zerox.d.ts +25 -0
- package/dist/lib/zerox.js +43 -0
- package/dist/output.d.ts +11 -0
- package/dist/output.js +46 -0
- package/dist/rpc.d.ts +5 -0
- package/dist/rpc.js +22 -0
- package/dist/wallet.d.ts +10 -0
- package/dist/wallet.js +28 -0
- package/package.json +46 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getSquid } from "../lib/squid.js";
|
|
3
|
+
import { listPendingBridges, removePendingBridges } from "../lib/pending-bridges.js";
|
|
4
|
+
import { getPendingTxList } from "../lib/alchemy-history.js";
|
|
5
|
+
import { getAddress } from "../wallet.js";
|
|
6
|
+
import { CHAIN_NAMES, SUPPORTED_CHAIN_IDS, EXPLORER_URLS, resolveChainId, getChainOptionsHint } from "../constants.js";
|
|
7
|
+
import { out, exitWithError, setJsonMode, isJsonMode, usageHint } from "../output.js";
|
|
8
|
+
export function pendingCmd() {
|
|
9
|
+
return new Command("pending")
|
|
10
|
+
.description("Show in-flight bridge transfers and pending txs")
|
|
11
|
+
.option("-c, --chain <id|name>", "Chain for pending txs only (omit = all chains)")
|
|
12
|
+
.option("--no-bridges", "Skip bridge status")
|
|
13
|
+
.option("--no-txs", "Skip pending tx list")
|
|
14
|
+
.option("--clear-done", "Remove completed/failed bridges from stored list")
|
|
15
|
+
.action(async function (opts) {
|
|
16
|
+
setJsonMode(this.parent?.opts().json ?? false);
|
|
17
|
+
const showBridges = opts.bridges !== false;
|
|
18
|
+
const showTxs = opts.txs !== false;
|
|
19
|
+
const clearDone = opts.clearDone === true;
|
|
20
|
+
const address = getAddress();
|
|
21
|
+
const result = {};
|
|
22
|
+
if (showBridges) {
|
|
23
|
+
const entries = listPendingBridges();
|
|
24
|
+
const integratorId = process.env.SQUID_INTEGRATOR_ID?.trim();
|
|
25
|
+
const bridgeStatuses = [];
|
|
26
|
+
const toRemove = [];
|
|
27
|
+
if (integratorId && entries.length > 0) {
|
|
28
|
+
try {
|
|
29
|
+
const squid = await getSquid();
|
|
30
|
+
const statusResults = await Promise.allSettled(entries.map((e) => squid.getStatus({
|
|
31
|
+
transactionId: e.txHash,
|
|
32
|
+
requestId: e.requestId,
|
|
33
|
+
integratorId,
|
|
34
|
+
quoteId: e.quoteId ?? "",
|
|
35
|
+
})));
|
|
36
|
+
for (let i = 0; i < entries.length; i++) {
|
|
37
|
+
const e = entries[i];
|
|
38
|
+
const r = statusResults[i];
|
|
39
|
+
if (r?.status === "fulfilled") {
|
|
40
|
+
const s = (r.value.squidTransactionStatus ?? "").toLowerCase();
|
|
41
|
+
bridgeStatuses.push({
|
|
42
|
+
requestId: e.requestId,
|
|
43
|
+
status: r.value.squidTransactionStatus,
|
|
44
|
+
fromChain: e.fromChain,
|
|
45
|
+
toChain: e.toChain,
|
|
46
|
+
txHash: e.txHash,
|
|
47
|
+
});
|
|
48
|
+
if (clearDone && (s.includes("success") || s.includes("complete") || s.includes("failed") || s.includes("error"))) {
|
|
49
|
+
toRemove.push(e.requestId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
bridgeStatuses.push({
|
|
54
|
+
requestId: e.requestId,
|
|
55
|
+
status: "unknown",
|
|
56
|
+
fromChain: e.fromChain,
|
|
57
|
+
toChain: e.toChain,
|
|
58
|
+
txHash: e.txHash,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (clearDone && toRemove.length > 0) {
|
|
63
|
+
removePendingBridges(toRemove);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
68
|
+
if (!isJsonMode())
|
|
69
|
+
out(`Bridge status error: ${msg}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
result.bridges = bridgeStatuses;
|
|
73
|
+
if (!isJsonMode()) {
|
|
74
|
+
if (bridgeStatuses.length === 0) {
|
|
75
|
+
out("No in-flight bridges (or none stored). Run 'speed bridge' to add one.");
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
out("Bridges:");
|
|
79
|
+
for (const b of bridgeStatuses) {
|
|
80
|
+
out(` ${b.fromChain} → ${b.toChain} ${b.status} ${b.txHash.slice(0, 10)}... requestId: ${b.requestId}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (showTxs) {
|
|
86
|
+
const resolvedChain = opts.chain !== undefined ? resolveChainId(opts.chain) : null;
|
|
87
|
+
const chainsToQuery = opts.chain !== undefined
|
|
88
|
+
? resolvedChain !== null
|
|
89
|
+
? [resolvedChain]
|
|
90
|
+
: []
|
|
91
|
+
: [...SUPPORTED_CHAIN_IDS];
|
|
92
|
+
if (opts.chain !== undefined && chainsToQuery.length === 0) {
|
|
93
|
+
exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}.${usageHint("pending")}`, "INVALID_CHAIN");
|
|
94
|
+
}
|
|
95
|
+
const txLists = await Promise.allSettled(chainsToQuery.map((chainId) => getPendingTxList(chainId, address)));
|
|
96
|
+
const pendingTxs = [];
|
|
97
|
+
for (let i = 0; i < chainsToQuery.length; i++) {
|
|
98
|
+
const chainId = chainsToQuery[i];
|
|
99
|
+
const result = txLists[i];
|
|
100
|
+
const list = result?.status === "fulfilled" ? result.value : [];
|
|
101
|
+
for (const tx of list) {
|
|
102
|
+
pendingTxs.push({ chainId, hash: tx.hash, from: tx.from, to: tx.to, value: tx.value });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
result.pendingTxs = pendingTxs;
|
|
106
|
+
if (!isJsonMode()) {
|
|
107
|
+
if (showBridges)
|
|
108
|
+
out("");
|
|
109
|
+
if (pendingTxs.length === 0) {
|
|
110
|
+
out("No pending txs (Alchemy pending is WebSocket-only).");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
out("Pending txs:");
|
|
114
|
+
for (const t of pendingTxs) {
|
|
115
|
+
const link = EXPLORER_URLS[t.chainId] ? `${EXPLORER_URLS[t.chainId]}/tx/${t.hash}` : t.hash;
|
|
116
|
+
out(` ${CHAIN_NAMES[t.chainId] ?? t.chainId} ${t.hash.slice(0, 10)}... ${link}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (isJsonMode()) {
|
|
122
|
+
out(result);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getOraclePrices, parseEthUsdRaw } from "../lib/oracle.js";
|
|
3
|
+
import { resolveChainId, getChainOptionsHint } from "../constants.js";
|
|
4
|
+
import { getDefaultChainInput } from "./config.js";
|
|
5
|
+
import { out, exitWithError, setJsonMode, isJsonMode, usageHint } from "../output.js";
|
|
6
|
+
export function priceCmd() {
|
|
7
|
+
return new Command("price")
|
|
8
|
+
.description("Speed/ETH, Speed/USD, native USD via Lightspeed oracles")
|
|
9
|
+
.option("-c, --chain <id|name>", "Chain ID or name", "8453")
|
|
10
|
+
.action(async function (opts) {
|
|
11
|
+
setJsonMode(this.parent?.opts().json ?? false);
|
|
12
|
+
const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
|
|
13
|
+
if (chainId === null) {
|
|
14
|
+
exitWithError(`Unknown or unsupported chain: ${opts.chain}. Use: ${getChainOptionsHint()}`, "INVALID_CHAIN");
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const { speedEth: speedEthRaw, speedUsd: speedUsdRaw, ethUsd: ethUsdRaw } = await getOraclePrices(chainId);
|
|
18
|
+
const speedEth = Number(speedEthRaw) / 1e18;
|
|
19
|
+
const speedUsd = Number(speedUsdRaw) / 1e8;
|
|
20
|
+
const ethUsd = parseEthUsdRaw(ethUsdRaw);
|
|
21
|
+
if (isJsonMode()) {
|
|
22
|
+
out({ chainId, speedPerNative: speedEth, speedUsd, nativeUsd: ethUsd });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
out(`Speed/Native: ${speedEth}`);
|
|
26
|
+
out(`Speed/USD: $${speedUsd}`);
|
|
27
|
+
out(`Native/USD: $${ethUsd}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
32
|
+
exitWithError(`${msg}.${usageHint("price")}`, "PRICE_ERROR");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { getAddress } from "../wallet.js";
|
|
4
|
+
import { get0xQuote } from "../lib/zerox.js";
|
|
5
|
+
import { parseTokenAmountToWei } from "../lib/parse-amount.js";
|
|
6
|
+
import { resolveChainId, getChainOptionsHint } from "../constants.js";
|
|
7
|
+
import { getDefaultChainInput } from "./config.js";
|
|
8
|
+
import { out, exitWithError, setJsonMode, isJsonMode, usageHint } from "../output.js";
|
|
9
|
+
export function quoteCmd() {
|
|
10
|
+
return new Command("quote")
|
|
11
|
+
.description("Preview swap (no tx). To execute, use: speed swap --buy <addr> -a <amount> (approve is automatic)")
|
|
12
|
+
.option("-c, --chain <id|name>", "Chain ID or name", "8453")
|
|
13
|
+
.option("--sell <address>", "Sell token address (default: Speed)")
|
|
14
|
+
.option("--buy <address>", "Buy token address")
|
|
15
|
+
.option("-a, --amount <amount>", "Sell amount in token units (e.g. 0.002, 10000)")
|
|
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()}.${usageHint("quote")}`, "INVALID_CHAIN");
|
|
21
|
+
}
|
|
22
|
+
if (!opts.buy || !opts.amount) {
|
|
23
|
+
exitWithError("--buy and --amount are required", "MISSING_ARGS");
|
|
24
|
+
}
|
|
25
|
+
const sellToken = opts.sell?.trim() ?? "0xB01CF1bE9568f09449382a47Cd5bF58e2A9D5922";
|
|
26
|
+
const buyToken = opts.buy.trim();
|
|
27
|
+
const amount = parseTokenAmountToWei(opts.amount.trim());
|
|
28
|
+
try {
|
|
29
|
+
const spinner = isJsonMode() ? null : ora("Fetching quote...").start();
|
|
30
|
+
const taker = getAddress();
|
|
31
|
+
const quote = await get0xQuote(chainId, sellToken, buyToken, amount, taker);
|
|
32
|
+
if (spinner)
|
|
33
|
+
spinner.succeed("Quote received");
|
|
34
|
+
if (isJsonMode()) {
|
|
35
|
+
out({
|
|
36
|
+
sellAmount: quote.sellAmount,
|
|
37
|
+
buyAmount: quote.buyAmount,
|
|
38
|
+
buyToken: quote.buyToken,
|
|
39
|
+
sellToken: quote.sellToken,
|
|
40
|
+
gas: quote.gas,
|
|
41
|
+
to: quote.transaction?.to,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
out(`Buy amount: ${quote.buyAmount}`);
|
|
46
|
+
out(`Gas estimate: ${quote.gas}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
51
|
+
exitWithError(`${msg}.${usageHint("quote")}`, "QUOTE_ERROR");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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, success, usageHint } from "../output.js";
|
|
7
|
+
const ERC20_ABI = ["function approve(address spender, uint256 amount) returns (bool)"];
|
|
8
|
+
export function revokeCmd() {
|
|
9
|
+
return new Command("revoke")
|
|
10
|
+
.description("Set allowance back to 0 for a spender (security hygiene)")
|
|
11
|
+
.option("-c, --chain <id|name>", "Chain ID or name", "8453")
|
|
12
|
+
.requiredOption("--token <address>", "Token contract address")
|
|
13
|
+
.requiredOption("--spender <address>", "Spender address to revoke (e.g. 0x AllowanceHolder)")
|
|
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 chain "${opts.chain}". Use one of: ${getChainOptionsHint()}. Example: speed revoke -c base --token <addr> --spender <addr>`, "INVALID_CHAIN");
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const signer = getSigner(chainId);
|
|
22
|
+
const token = new ethers.Contract(opts.token.trim(), ERC20_ABI, signer);
|
|
23
|
+
const tx = await token.approve(opts.spender.trim(), 0n);
|
|
24
|
+
await tx.wait();
|
|
25
|
+
if (isJsonMode()) {
|
|
26
|
+
out({ txHash: tx.hash, token: opts.token, spender: opts.spender });
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
success(`Revoked allowance for ${opts.spender} on ${opts.token}`);
|
|
30
|
+
out(`Tx: ${tx.hash}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
35
|
+
exitWithError(`${msg}.${usageHint("revoke")}`, "REVOKE_ERROR");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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 { EXPLORER_URLS, SPEED_TOKEN_ADDRESS, 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 transfer(address to, uint256 amount) returns (bool)"];
|
|
9
|
+
export function sendCmd() {
|
|
10
|
+
return new Command("send")
|
|
11
|
+
.description("Plain ERC-20 transfer of Speed to an address")
|
|
12
|
+
.option("-c, --chain <id|name>", "Chain ID or name", "8453")
|
|
13
|
+
.requiredOption("-t, --to <address>", "Recipient address")
|
|
14
|
+
.requiredOption("-a, --amount <amount>", "Amount of SPEED to send (e.g. 0.1, 1000)")
|
|
15
|
+
.action(async function (opts) {
|
|
16
|
+
setJsonMode(this.parent?.opts().json ?? false);
|
|
17
|
+
const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
|
|
18
|
+
if (chainId === null) {
|
|
19
|
+
exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}. Example: speed send -c base -t <to-address> -a 1000`, "INVALID_CHAIN");
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const signer = getSigner(chainId);
|
|
23
|
+
const token = new ethers.Contract(SPEED_TOKEN_ADDRESS, ERC20_ABI, signer);
|
|
24
|
+
const amountWei = parseTokenAmountToWei(opts.amount.trim());
|
|
25
|
+
const tx = await token.transfer(opts.to.trim(), amountWei);
|
|
26
|
+
await tx.wait();
|
|
27
|
+
const explorer = EXPLORER_URLS[chainId];
|
|
28
|
+
const link = explorer ? `${explorer}/tx/${tx.hash}` : tx.hash;
|
|
29
|
+
if (isJsonMode()) {
|
|
30
|
+
out({ txHash: tx.hash, explorerLink: link });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
success(`Sent. Tx: ${tx.hash}`);
|
|
34
|
+
if (explorer)
|
|
35
|
+
out(link);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
40
|
+
exitWithError(`${msg}.${usageHint("send")}`, "SEND_ERROR");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
4
|
+
import { ethers } from "ethers";
|
|
5
|
+
import { getEnvPath, getSpeedConfigDir } from "../env.js";
|
|
6
|
+
import { out, exitWithError, setJsonMode, isJsonMode, success } from "../output.js";
|
|
7
|
+
const ENV_KEYS = [
|
|
8
|
+
{ key: "PRIVATE_KEY", label: "Wallet private key (hex, or leave blank to generate a new bot wallet)", required: false },
|
|
9
|
+
{ key: "0X_API_KEY", label: "0x API key (https://dashboard.0x.org/)", required: true },
|
|
10
|
+
{ key: "SQUID_INTEGRATOR_ID", label: "Squid integrator ID (https://docs.squidrouter.com)", required: true },
|
|
11
|
+
{ key: "ALCHEMY_API_KEY", label: "Alchemy API key (https://dashboard.alchemy.com/)", required: true },
|
|
12
|
+
];
|
|
13
|
+
function question(rl, prompt) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
rl.question(prompt, (answer) => {
|
|
16
|
+
resolve((answer ?? "").trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function escapeEnvValue(value) {
|
|
21
|
+
const trimmed = value.replace(/\r?\n/g, "").trim();
|
|
22
|
+
if (trimmed === "")
|
|
23
|
+
return '""';
|
|
24
|
+
if (/[\s#"\\]/.test(trimmed)) {
|
|
25
|
+
return '"' + trimmed.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
export function setupCmd() {
|
|
30
|
+
return new Command("setup")
|
|
31
|
+
.description("Interactively set secrets; writes to ~/.speed/.env (run when env is not set)")
|
|
32
|
+
.option("--force", "Overwrite existing .env (default: prompt to merge)")
|
|
33
|
+
.action(async function (opts) {
|
|
34
|
+
setJsonMode(this.parent?.opts().json ?? false);
|
|
35
|
+
if (isJsonMode()) {
|
|
36
|
+
exitWithError("Setup is interactive; omit --json. Example: speed setup", "INVALID_MODE");
|
|
37
|
+
}
|
|
38
|
+
const envPath = getEnvPath();
|
|
39
|
+
const dir = getSpeedConfigDir();
|
|
40
|
+
const existing = {};
|
|
41
|
+
if (existsSync(envPath)) {
|
|
42
|
+
const raw = readFileSync(envPath, "utf-8");
|
|
43
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
44
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
45
|
+
if (m) {
|
|
46
|
+
const v = m[2].replace(/^["']|["']$/g, "").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
47
|
+
existing[m[1]] = v;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!opts.force) {
|
|
51
|
+
out(`Found existing ${envPath}`);
|
|
52
|
+
out("Leave a prompt blank to keep current value.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
const next = { ...existing };
|
|
57
|
+
for (const { key, label, required } of ENV_KEYS) {
|
|
58
|
+
const current = existing[key];
|
|
59
|
+
const hint = current ? ` [current: ${current.slice(0, 8)}...]` : "";
|
|
60
|
+
const prompt = `${label}${hint}: `;
|
|
61
|
+
const answer = await question(rl, prompt);
|
|
62
|
+
if (answer !== "") {
|
|
63
|
+
next[key] = answer;
|
|
64
|
+
}
|
|
65
|
+
else if (current) {
|
|
66
|
+
next[key] = current;
|
|
67
|
+
}
|
|
68
|
+
else if (required) {
|
|
69
|
+
rl.close();
|
|
70
|
+
exitWithError(`Missing required value for ${key}. Re-run: speed setup`, "SETUP_INCOMPLETE");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
rl.close();
|
|
74
|
+
if (!next.PRIVATE_KEY?.trim()) {
|
|
75
|
+
const wallet = ethers.Wallet.createRandom();
|
|
76
|
+
const pk = wallet.privateKey;
|
|
77
|
+
next.PRIVATE_KEY = pk.startsWith("0x") ? pk : `0x${pk}`;
|
|
78
|
+
if (!isJsonMode()) {
|
|
79
|
+
success("Created new bot wallet (no private key provided).");
|
|
80
|
+
out(`Address: ${wallet.address}`);
|
|
81
|
+
out("Fund this address to use swap/bridge/send.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const missing = ENV_KEYS.filter(({ key, required }) => required && !next[key]);
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
exitWithError(`Missing required: ${missing.map((m) => m.key).join(", ")}. Re-run: speed setup`, "SETUP_INCOMPLETE");
|
|
87
|
+
}
|
|
88
|
+
const apiKey = next["0X_API_KEY"] ?? "";
|
|
89
|
+
const lines = [
|
|
90
|
+
"# Generated by speed setup. Do not commit.",
|
|
91
|
+
"",
|
|
92
|
+
`PRIVATE_KEY=${escapeEnvValue(next.PRIVATE_KEY ?? "")}`,
|
|
93
|
+
`0X_API_KEY=${escapeEnvValue(apiKey)}`,
|
|
94
|
+
`OX_API_KEY=${escapeEnvValue(apiKey)}`,
|
|
95
|
+
`SQUID_INTEGRATOR_ID=${escapeEnvValue(next.SQUID_INTEGRATOR_ID ?? "")}`,
|
|
96
|
+
`ALCHEMY_API_KEY=${escapeEnvValue(next.ALCHEMY_API_KEY ?? "")}`,
|
|
97
|
+
"",
|
|
98
|
+
];
|
|
99
|
+
mkdirSync(dir, { recursive: true });
|
|
100
|
+
writeFileSync(envPath, lines.join("\n"), "utf-8");
|
|
101
|
+
success(`Wrote ${envPath}`);
|
|
102
|
+
out("Run 'speed whoami' or 'speed doctor' to verify.");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getSquid } from "../lib/squid.js";
|
|
3
|
+
import { resolveChainId, getChainOptionsHint } from "../constants.js";
|
|
4
|
+
import { getDefaultChainInput } from "./config.js";
|
|
5
|
+
import { out, exitWithError, setJsonMode, isJsonMode, usageHint } from "../output.js";
|
|
6
|
+
export function statusCmd() {
|
|
7
|
+
return new Command("status")
|
|
8
|
+
.description("Check tx confirmation; for bridge, poll Squid getStatus")
|
|
9
|
+
.requiredOption("--tx <hash>", "Transaction hash")
|
|
10
|
+
.option("-c, --chain <id|name>", "Chain ID or name (for non-bridge tx)")
|
|
11
|
+
.option("--request-id <id>", "Squid request ID (for bridge)")
|
|
12
|
+
.option("--quote-id <id>", "Squid quote ID (for bridge)")
|
|
13
|
+
.action(async function (opts) {
|
|
14
|
+
setJsonMode(this.parent?.opts().json ?? false);
|
|
15
|
+
const txHash = opts.tx.trim();
|
|
16
|
+
const integratorId = process.env.SQUID_INTEGRATOR_ID?.trim();
|
|
17
|
+
if (opts.requestId && integratorId) {
|
|
18
|
+
try {
|
|
19
|
+
const squid = await getSquid();
|
|
20
|
+
const status = await squid.getStatus({
|
|
21
|
+
transactionId: txHash,
|
|
22
|
+
requestId: opts.requestId,
|
|
23
|
+
integratorId,
|
|
24
|
+
quoteId: opts.quoteId ?? "",
|
|
25
|
+
});
|
|
26
|
+
if (isJsonMode()) {
|
|
27
|
+
out({ status: status.squidTransactionStatus, ...status });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
out(`Bridge status: ${status.squidTransactionStatus}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
35
|
+
exitWithError(`${msg}.${usageHint("status")}`, "STATUS_ERROR");
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const chainId = resolveChainId(opts.chain ?? getDefaultChainInput());
|
|
40
|
+
if (chainId === null) {
|
|
41
|
+
exitWithError(`Unknown chain "${opts.chain}". Use one of: ${getChainOptionsHint()}. Example: speed status --tx <hash> -c base`, "INVALID_CHAIN");
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const { getSigner } = await import("../wallet.js");
|
|
45
|
+
const { getRpcUrl } = await import("../rpc.js");
|
|
46
|
+
const { ethers } = await import("ethers");
|
|
47
|
+
const provider = new ethers.JsonRpcProvider(getRpcUrl(chainId));
|
|
48
|
+
const receipt = await provider.getTransactionReceipt(txHash);
|
|
49
|
+
if (isJsonMode()) {
|
|
50
|
+
out({
|
|
51
|
+
blockNumber: receipt?.blockNumber?.toString(),
|
|
52
|
+
status: receipt?.status === 1 ? "confirmed" : receipt ? "reverted" : "pending",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
out(receipt ? `Status: ${receipt.status === 1 ? "confirmed" : "reverted"} Block: ${receipt.blockNumber}` : "Pending or not found");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
61
|
+
exitWithError(`${msg}.${usageHint("status")}`, "STATUS_ERROR");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|