@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,135 @@
|
|
|
1
|
+
/** Speed Token address (same on supported chains where applicable) */
|
|
2
|
+
export const SPEED_TOKEN_ADDRESS = "0xB01CF1bE9568f09449382a47Cd5bF58e2A9D5922";
|
|
3
|
+
/** 0x API uses this address for native ETH (sell-token in quotes) */
|
|
4
|
+
export const NATIVE_ETH_0X = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
5
|
+
/** Supported chain IDs: Ethereum, Base, OP, Arbitrum, Polygon, BNB */
|
|
6
|
+
export const SUPPORTED_CHAIN_IDS = [1, 8453, 10, 42161, 137, 56];
|
|
7
|
+
/** Chain ID to primary display name */
|
|
8
|
+
export const CHAIN_NAMES = {
|
|
9
|
+
1: "ethereum",
|
|
10
|
+
8453: "base",
|
|
11
|
+
10: "optimism",
|
|
12
|
+
42161: "arbitrum",
|
|
13
|
+
137: "polygon",
|
|
14
|
+
56: "bnb",
|
|
15
|
+
};
|
|
16
|
+
/** Name/alias (lowercase) to chain ID. Use resolveChainId() for parsing. */
|
|
17
|
+
export const CHAIN_NAME_TO_ID = {
|
|
18
|
+
ethereum: 1,
|
|
19
|
+
eth: 1,
|
|
20
|
+
mainnet: 1,
|
|
21
|
+
base: 8453,
|
|
22
|
+
optimism: 10,
|
|
23
|
+
op: 10,
|
|
24
|
+
arbitrum: 42161,
|
|
25
|
+
arb: 42161,
|
|
26
|
+
polygon: 137,
|
|
27
|
+
matic: 137,
|
|
28
|
+
bnb: 56,
|
|
29
|
+
bsc: 56,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Resolve chain from ID number or name (e.g. "8453", "base", "ethereum").
|
|
33
|
+
* Returns the chain ID if supported, otherwise null.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveChainId(input) {
|
|
36
|
+
const s = input.trim();
|
|
37
|
+
if (/^\d+$/.test(s)) {
|
|
38
|
+
const id = parseInt(s, 10);
|
|
39
|
+
return SUPPORTED_CHAIN_IDS.includes(id) ? id : null;
|
|
40
|
+
}
|
|
41
|
+
const id = CHAIN_NAME_TO_ID[s.toLowerCase()];
|
|
42
|
+
return id != null && SUPPORTED_CHAIN_IDS.includes(id) ? id : null;
|
|
43
|
+
}
|
|
44
|
+
/** Resolve token option: empty or "speed" → SPEED_TOKEN_ADDRESS, else return trimmed address. */
|
|
45
|
+
export function resolveTokenAddress(input) {
|
|
46
|
+
if (!input?.trim())
|
|
47
|
+
return SPEED_TOKEN_ADDRESS;
|
|
48
|
+
if (input.trim().toLowerCase() === "speed")
|
|
49
|
+
return SPEED_TOKEN_ADDRESS;
|
|
50
|
+
return input.trim();
|
|
51
|
+
}
|
|
52
|
+
/** Human-readable list of supported chains for error messages (e.g. "1 (ethereum), 8453 (base), ...") */
|
|
53
|
+
export function getChainOptionsHint() {
|
|
54
|
+
return SUPPORTED_CHAIN_IDS.map((id) => `${id} (${CHAIN_NAMES[id]})`).join(", ");
|
|
55
|
+
}
|
|
56
|
+
/** Alchemy chain prefix for RPC URL */
|
|
57
|
+
export const ALCHEMY_CHAIN_PREFIX = {
|
|
58
|
+
1: "eth",
|
|
59
|
+
10: "opt",
|
|
60
|
+
137: "polygon",
|
|
61
|
+
42161: "arb",
|
|
62
|
+
56: "bnb",
|
|
63
|
+
8453: "base",
|
|
64
|
+
};
|
|
65
|
+
/** One public fallback RPC per chain when Alchemy is not set */
|
|
66
|
+
export const PUBLIC_RPC_FALLBACKS = {
|
|
67
|
+
1: "https://eth.llamarpc.com",
|
|
68
|
+
10: "https://mainnet.optimism.io",
|
|
69
|
+
137: "https://polygon.llamarpc.com",
|
|
70
|
+
42161: "https://arb1.arbitrum.io/rpc",
|
|
71
|
+
56: "https://bsc-dataseed.binance.org",
|
|
72
|
+
8453: "https://mainnet.base.org",
|
|
73
|
+
};
|
|
74
|
+
/** Lightspeed price oracle addresses by chain (same ABI on all) */
|
|
75
|
+
export const ORACLE_ADDRESSES = {
|
|
76
|
+
8453: "0xFc89e7541339499a3c3e0030825DaaD67a52CBd4",
|
|
77
|
+
1: "0x73f7a22c2fa57c09d292c20627a6e01c791b95ea",
|
|
78
|
+
56: "0x2529107fdb6d2dee755540653e538ccb99641d55",
|
|
79
|
+
10: "0x8c584569fcbfbd0b57c991973b92a981a23deaf9",
|
|
80
|
+
42161: "0x11a1ad1d0d83cd85cc37b7be9b98d37ec07bf66c",
|
|
81
|
+
137: "0x3e3b0dCdF39F5D9D2179eB72fE1E71D303523941",
|
|
82
|
+
};
|
|
83
|
+
/** Minimal ABI for oracle reads: getLatestSpeedEthPrice, getLatestSpeedUsdPrice, getEthUsdPrice */
|
|
84
|
+
export const ORACLE_ABI = [
|
|
85
|
+
{
|
|
86
|
+
inputs: [],
|
|
87
|
+
name: "getLatestSpeedEthPrice",
|
|
88
|
+
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
|
|
89
|
+
stateMutability: "view",
|
|
90
|
+
type: "function",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
inputs: [],
|
|
94
|
+
name: "getLatestSpeedUsdPrice",
|
|
95
|
+
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
|
|
96
|
+
stateMutability: "view",
|
|
97
|
+
type: "function",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
inputs: [],
|
|
101
|
+
name: "getEthUsdPrice",
|
|
102
|
+
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
|
|
103
|
+
stateMutability: "view",
|
|
104
|
+
type: "function",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
export const ZEROX_API_BASE = "https://api.0x.org";
|
|
108
|
+
export const SQUID_BASE_URL = "https://v2.api.squidrouter.com";
|
|
109
|
+
/** Native token symbol per chain (for balance display) */
|
|
110
|
+
export const NATIVE_SYMBOL = {
|
|
111
|
+
1: "ETH",
|
|
112
|
+
8453: "ETH",
|
|
113
|
+
10: "ETH",
|
|
114
|
+
42161: "ETH",
|
|
115
|
+
137: "MATIC",
|
|
116
|
+
56: "BNB",
|
|
117
|
+
};
|
|
118
|
+
/** Block explorer base URLs by chain for tx links */
|
|
119
|
+
export const EXPLORER_URLS = {
|
|
120
|
+
1: "https://etherscan.io",
|
|
121
|
+
8453: "https://basescan.org",
|
|
122
|
+
10: "https://optimistic.etherscan.io",
|
|
123
|
+
42161: "https://arbiscan.io",
|
|
124
|
+
137: "https://polygonscan.com",
|
|
125
|
+
56: "https://bscscan.com",
|
|
126
|
+
};
|
|
127
|
+
/** Wrapped native token address per chain (for gas command: swap Speed → native) */
|
|
128
|
+
export const WRAPPED_NATIVE_ADDRESS = {
|
|
129
|
+
1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
130
|
+
8453: "0x4200000000000000000000000000000000000006",
|
|
131
|
+
10: "0x4200000000000000000000000000000000000006",
|
|
132
|
+
42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
133
|
+
137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
134
|
+
56: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
135
|
+
};
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Path to ~/.speed (or SPEED_CONFIG_DIR). */
|
|
2
|
+
export declare function getSpeedConfigDir(): string;
|
|
3
|
+
/** Path to ~/.speed/.env (secrets from `speed setup`). */
|
|
4
|
+
export declare function getEnvPath(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Load env: first ~/.speed/.env (from speed setup), then .env in cwd.
|
|
7
|
+
* Cwd .env overrides. Call once at CLI startup.
|
|
8
|
+
* Keys like 0X_API_KEY are applied manually because dotenv does not load names starting with a digit.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadEnv(): void;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { config as dotenvConfig } from "dotenv";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
const SPEED_CONFIG_DIR = process.env.SPEED_CONFIG_DIR ?? join(homedir(), ".speed");
|
|
6
|
+
/** Path to ~/.speed (or SPEED_CONFIG_DIR). */
|
|
7
|
+
export function getSpeedConfigDir() {
|
|
8
|
+
return SPEED_CONFIG_DIR;
|
|
9
|
+
}
|
|
10
|
+
/** Path to ~/.speed/.env (secrets from `speed setup`). */
|
|
11
|
+
export function getEnvPath() {
|
|
12
|
+
return join(SPEED_CONFIG_DIR, ".env");
|
|
13
|
+
}
|
|
14
|
+
/** Dotenv skips keys that start with a digit (e.g. 0X_API_KEY). Apply from file manually. */
|
|
15
|
+
function applyNumericPrefixedKeys(envPath) {
|
|
16
|
+
try {
|
|
17
|
+
let raw = readFileSync(envPath, "utf-8");
|
|
18
|
+
raw = raw.replace(/^\uFEFF/, ""); // strip BOM (Windows can add it)
|
|
19
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (trimmed.startsWith("#"))
|
|
22
|
+
continue;
|
|
23
|
+
const idx = trimmed.indexOf("0X_API_KEY=");
|
|
24
|
+
const idxAlt = trimmed.indexOf("OX_API_KEY=");
|
|
25
|
+
const keyStart = idx >= 0 ? idx : idxAlt >= 0 ? idxAlt : -1;
|
|
26
|
+
if (keyStart >= 0) {
|
|
27
|
+
const eq = trimmed.indexOf("=", keyStart);
|
|
28
|
+
const rest = trimmed.slice(eq + 1).trim();
|
|
29
|
+
const v = rest.replace(/^["']|["']$/g, "").replace(/\\"/g, '"').replace(/\\\\/g, "\\").trim();
|
|
30
|
+
if (v !== "") {
|
|
31
|
+
process.env["0X_API_KEY"] = v;
|
|
32
|
+
process.env["OX_API_KEY"] = v;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignore read errors (e.g. missing file, permissions)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load env: first ~/.speed/.env (from speed setup), then .env in cwd.
|
|
44
|
+
* Cwd .env overrides. Call once at CLI startup.
|
|
45
|
+
* Keys like 0X_API_KEY are applied manually because dotenv does not load names starting with a digit.
|
|
46
|
+
*/
|
|
47
|
+
export function loadEnv() {
|
|
48
|
+
const globalPath = getEnvPath();
|
|
49
|
+
applyNumericPrefixedKeys(globalPath); // always try (dotenv skips 0X_API_KEY; file may exist even if path differs)
|
|
50
|
+
if (existsSync(globalPath)) {
|
|
51
|
+
dotenvConfig({ path: globalPath });
|
|
52
|
+
}
|
|
53
|
+
dotenvConfig(); // cwd .env
|
|
54
|
+
const cwdEnv = join(process.cwd(), ".env");
|
|
55
|
+
if (existsSync(cwdEnv)) {
|
|
56
|
+
applyNumericPrefixedKeys(cwdEnv); // cwd can override (empty values do not override)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Normalized transfer/tx for history display (Alchemy getAssetTransfers shape). */
|
|
2
|
+
export interface TransferEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
value: string;
|
|
7
|
+
blockNumber: string;
|
|
8
|
+
timeStamp: string;
|
|
9
|
+
category?: string;
|
|
10
|
+
/** "in" | "out" relative to the queried address */
|
|
11
|
+
direction?: "in" | "out";
|
|
12
|
+
/** Display symbol: ETH, SPEED, or token address slice */
|
|
13
|
+
symbol?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Fetch transfer history for address via Alchemy getAssetTransfers (uses ALCHEMY_API_KEY / getRpcUrl). */
|
|
16
|
+
export declare function getTransfers(chainId: number, address: string, options?: {
|
|
17
|
+
limit?: number;
|
|
18
|
+
pageKey?: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
transfers: TransferEntry[];
|
|
21
|
+
pageKey?: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** Pending txs for address: Alchemy exposes this only via WebSocket (alchemy_pendingTransactions). Return empty for REST. */
|
|
24
|
+
export declare function getPendingTxList(_chainId: number, _address: string): Promise<TransferEntry[]>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getRpcUrl } from "../rpc.js";
|
|
2
|
+
import { SPEED_TOKEN_ADDRESS } from "../constants.js";
|
|
3
|
+
/** Fetch transfer history for address via Alchemy getAssetTransfers (uses ALCHEMY_API_KEY / getRpcUrl). */
|
|
4
|
+
export async function getTransfers(chainId, address, options = {}) {
|
|
5
|
+
const url = getRpcUrl(chainId);
|
|
6
|
+
const maxCount = Math.min(options.limit ?? 20, 100);
|
|
7
|
+
// Alchemy: 'internal' category only supported for ETH (1) and MATIC (137)
|
|
8
|
+
const categories = chainId === 1 || chainId === 137
|
|
9
|
+
? ["external", "internal", "erc20", "erc721", "erc1155"]
|
|
10
|
+
: ["external", "erc20", "erc721", "erc1155"];
|
|
11
|
+
const params = {
|
|
12
|
+
fromAddress: address,
|
|
13
|
+
fromBlock: "0x0",
|
|
14
|
+
toBlock: "latest",
|
|
15
|
+
maxCount: "0x" + maxCount.toString(16),
|
|
16
|
+
category: categories,
|
|
17
|
+
order: "desc",
|
|
18
|
+
withMetadata: true,
|
|
19
|
+
};
|
|
20
|
+
if (options.pageKey)
|
|
21
|
+
params.pageKey = options.pageKey;
|
|
22
|
+
const body = {
|
|
23
|
+
jsonrpc: "2.0",
|
|
24
|
+
id: 1,
|
|
25
|
+
method: "alchemy_getAssetTransfers",
|
|
26
|
+
params: [params],
|
|
27
|
+
};
|
|
28
|
+
const res = await fetch(url, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new Error(`Alchemy API error: ${res.status}`);
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
if (data.error)
|
|
37
|
+
throw new Error(`Alchemy: ${data.error.message}`);
|
|
38
|
+
const result = data.result;
|
|
39
|
+
if (!result?.transfers)
|
|
40
|
+
return { transfers: [] };
|
|
41
|
+
const addrLower = address.toLowerCase();
|
|
42
|
+
const transfers = result.transfers.map((t) => {
|
|
43
|
+
const from = (t.from ?? "").toLowerCase();
|
|
44
|
+
const to = (t.to ?? "").toLowerCase();
|
|
45
|
+
const direction = to === addrLower ? "in" : from === addrLower ? "out" : "in";
|
|
46
|
+
let symbol = t.asset ?? "";
|
|
47
|
+
if (!symbol && t.category) {
|
|
48
|
+
if (t.category === "external" || t.category === "internal")
|
|
49
|
+
symbol = "ETH";
|
|
50
|
+
else if (t.category === "erc20" && t.rawContract?.address) {
|
|
51
|
+
symbol = t.rawContract.address.toLowerCase() === SPEED_TOKEN_ADDRESS.toLowerCase() ? "SPEED" : `${t.rawContract.address.slice(0, 6)}…${t.rawContract.address.slice(-4)}`;
|
|
52
|
+
}
|
|
53
|
+
else if (t.category === "erc20")
|
|
54
|
+
symbol = "ERC20";
|
|
55
|
+
else
|
|
56
|
+
symbol = t.category.toUpperCase();
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
hash: t.hash ?? "",
|
|
60
|
+
from: t.from ?? "",
|
|
61
|
+
to: t.to ?? "",
|
|
62
|
+
value: t.value != null ? String(t.value) : "0",
|
|
63
|
+
blockNumber: t.blockNum ?? "0",
|
|
64
|
+
timeStamp: t.metadata?.blockTimestamp ?? "",
|
|
65
|
+
category: t.category,
|
|
66
|
+
direction,
|
|
67
|
+
symbol: symbol || undefined,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return { transfers, pageKey: result.pageKey };
|
|
71
|
+
}
|
|
72
|
+
/** Pending txs for address: Alchemy exposes this only via WebSocket (alchemy_pendingTransactions). Return empty for REST. */
|
|
73
|
+
export async function getPendingTxList(_chainId, _address) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ExplorerTx {
|
|
2
|
+
hash: string;
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
value: string;
|
|
6
|
+
gasUsed: string;
|
|
7
|
+
gasPrice: string;
|
|
8
|
+
blockNumber: string;
|
|
9
|
+
timeStamp: string;
|
|
10
|
+
txreceipt_status?: string;
|
|
11
|
+
isError?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ExplorerTxListResponse {
|
|
14
|
+
status: string;
|
|
15
|
+
message: string;
|
|
16
|
+
result: ExplorerTx[] | string;
|
|
17
|
+
}
|
|
18
|
+
/** Fetch normal transaction list for address (Etherscan-compatible API). */
|
|
19
|
+
export declare function getTxList(chainId: number, address: string, options?: {
|
|
20
|
+
limit?: number;
|
|
21
|
+
page?: number;
|
|
22
|
+
}): Promise<ExplorerTx[]>;
|
|
23
|
+
/** Fetch pending transactions for address (if supported by explorer). */
|
|
24
|
+
export declare function getPendingTxList(chainId: number, address: string): Promise<ExplorerTx[]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EXPLORER_API_BASES } from "../constants.js";
|
|
2
|
+
const API_KEY = process.env.EXPLORER_API_KEY ?? process.env.ETHERSCAN_API_KEY;
|
|
3
|
+
/** Fetch normal transaction list for address (Etherscan-compatible API). */
|
|
4
|
+
export async function getTxList(chainId, address, options = {}) {
|
|
5
|
+
const base = EXPLORER_API_BASES[chainId];
|
|
6
|
+
if (!base)
|
|
7
|
+
throw new Error(`No explorer API for chain ${chainId}`);
|
|
8
|
+
const limit = Math.min(options.limit ?? 20, 100);
|
|
9
|
+
const page = options.page ?? 1;
|
|
10
|
+
const params = new URLSearchParams({
|
|
11
|
+
module: "account",
|
|
12
|
+
action: "txlist",
|
|
13
|
+
address,
|
|
14
|
+
startblock: "0",
|
|
15
|
+
endblock: "99999999",
|
|
16
|
+
sort: "desc",
|
|
17
|
+
page: String(page),
|
|
18
|
+
offset: String(limit),
|
|
19
|
+
});
|
|
20
|
+
if (API_KEY?.trim())
|
|
21
|
+
params.set("apikey", API_KEY.trim());
|
|
22
|
+
const url = `${base}/api?${params}`;
|
|
23
|
+
const res = await fetch(url);
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`Explorer API error: ${res.status}`);
|
|
26
|
+
const data = (await res.json());
|
|
27
|
+
if (data.message !== "OK" && data.message !== "No transactions found") {
|
|
28
|
+
throw new Error(`Explorer API: ${data.message}`);
|
|
29
|
+
}
|
|
30
|
+
const result = data.result;
|
|
31
|
+
if (typeof result === "string")
|
|
32
|
+
return [];
|
|
33
|
+
return Array.isArray(result) ? result : [];
|
|
34
|
+
}
|
|
35
|
+
/** Fetch pending transactions for address (if supported by explorer). */
|
|
36
|
+
export async function getPendingTxList(chainId, address) {
|
|
37
|
+
const base = EXPLORER_API_BASES[chainId];
|
|
38
|
+
if (!base)
|
|
39
|
+
throw new Error(`No explorer API for chain ${chainId}`);
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
module: "account",
|
|
42
|
+
action: "pendingtxlist",
|
|
43
|
+
address,
|
|
44
|
+
});
|
|
45
|
+
if (API_KEY?.trim())
|
|
46
|
+
params.set("apikey", API_KEY.trim());
|
|
47
|
+
const url = `${base}/api?${params}`;
|
|
48
|
+
const res = await fetch(url);
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
return [];
|
|
51
|
+
const data = (await res.json());
|
|
52
|
+
if (data.message !== "OK" && data.message !== "No transactions found") {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const result = data.result;
|
|
56
|
+
if (typeof result === "string")
|
|
57
|
+
return [];
|
|
58
|
+
return Array.isArray(result) ? result : [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oracle USD prices may use 8 or 18 decimals. Normalize to a plain USD number.
|
|
3
|
+
* - raw >= 1e15: treat as 18 decimals (e.g. 2000e18 = $2000).
|
|
4
|
+
* - Else try 8 decimals; if result > MAX_PLAUSIBLE_PRICE, assume 18 decimals (e.g. low-priced token stored in 18 decimals).
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseEthUsdRaw(raw: bigint): number;
|
|
7
|
+
/** Get native token (ETH/BNB/POL) price in USD from Lightspeed oracle (raw uint256). */
|
|
8
|
+
export declare function getEthUsdPrice(chainId: number): Promise<bigint>;
|
|
9
|
+
/** Get ETH/USD price as a number (handles 8 or 18 decimals from oracle). */
|
|
10
|
+
export declare function getEthUsdPriceNumber(chainId: number): Promise<number>;
|
|
11
|
+
/** Get Speed/USD price (raw) */
|
|
12
|
+
export declare function getSpeedUsdPrice(chainId: number): Promise<bigint>;
|
|
13
|
+
/** Get Speed/USD price as a number (handles 8 or 18 decimals). */
|
|
14
|
+
export declare function getSpeedUsdPriceNumber(chainId: number): Promise<number>;
|
|
15
|
+
/** Get Speed/native price (raw) */
|
|
16
|
+
export declare function getSpeedEthPrice(chainId: number): Promise<bigint>;
|
|
17
|
+
/** All three oracle prices in one shot (one provider, parallel reads). Use for price command. */
|
|
18
|
+
export declare function getOraclePrices(chainId: number): Promise<{
|
|
19
|
+
ethUsd: bigint;
|
|
20
|
+
speedUsd: bigint;
|
|
21
|
+
speedEth: bigint;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { getRpcUrl } from "../rpc.js";
|
|
3
|
+
import { ORACLE_ADDRESSES, ORACLE_ABI } from "../constants.js";
|
|
4
|
+
/** Max plausible USD per unit (ETH/token); above this we assume wrong decimals. */
|
|
5
|
+
const MAX_PLAUSIBLE_PRICE = 1e6;
|
|
6
|
+
/**
|
|
7
|
+
* Oracle USD prices may use 8 or 18 decimals. Normalize to a plain USD number.
|
|
8
|
+
* - raw >= 1e15: treat as 18 decimals (e.g. 2000e18 = $2000).
|
|
9
|
+
* - Else try 8 decimals; if result > MAX_PLAUSIBLE_PRICE, assume 18 decimals (e.g. low-priced token stored in 18 decimals).
|
|
10
|
+
*/
|
|
11
|
+
export function parseEthUsdRaw(raw) {
|
|
12
|
+
const n = Number(raw);
|
|
13
|
+
if (n >= 1e15)
|
|
14
|
+
return n / 1e18;
|
|
15
|
+
const as8 = n / 1e8;
|
|
16
|
+
if (as8 > MAX_PLAUSIBLE_PRICE)
|
|
17
|
+
return n / 1e18; // low-priced token in 18 decimals (e.g. 1e13 → 0.00001)
|
|
18
|
+
return as8;
|
|
19
|
+
}
|
|
20
|
+
/** Get native token (ETH/BNB/POL) price in USD from Lightspeed oracle (raw uint256). */
|
|
21
|
+
export async function getEthUsdPrice(chainId) {
|
|
22
|
+
const addr = ORACLE_ADDRESSES[chainId];
|
|
23
|
+
if (!addr)
|
|
24
|
+
throw new Error(`No oracle for chain ${chainId}`);
|
|
25
|
+
const provider = new ethers.JsonRpcProvider(getRpcUrl(chainId));
|
|
26
|
+
const contract = new ethers.Contract(addr, ORACLE_ABI, provider);
|
|
27
|
+
return contract.getEthUsdPrice();
|
|
28
|
+
}
|
|
29
|
+
/** Get ETH/USD price as a number (handles 8 or 18 decimals from oracle). */
|
|
30
|
+
export async function getEthUsdPriceNumber(chainId) {
|
|
31
|
+
const raw = await getEthUsdPrice(chainId);
|
|
32
|
+
return parseEthUsdRaw(raw);
|
|
33
|
+
}
|
|
34
|
+
/** Get Speed/USD price (raw) */
|
|
35
|
+
export async function getSpeedUsdPrice(chainId) {
|
|
36
|
+
const addr = ORACLE_ADDRESSES[chainId];
|
|
37
|
+
if (!addr)
|
|
38
|
+
throw new Error(`No oracle for chain ${chainId}`);
|
|
39
|
+
const provider = new ethers.JsonRpcProvider(getRpcUrl(chainId));
|
|
40
|
+
const contract = new ethers.Contract(addr, ORACLE_ABI, provider);
|
|
41
|
+
return contract.getLatestSpeedUsdPrice();
|
|
42
|
+
}
|
|
43
|
+
/** Get Speed/USD price as a number (handles 8 or 18 decimals). */
|
|
44
|
+
export async function getSpeedUsdPriceNumber(chainId) {
|
|
45
|
+
const raw = await getSpeedUsdPrice(chainId);
|
|
46
|
+
return parseEthUsdRaw(raw);
|
|
47
|
+
}
|
|
48
|
+
/** Get Speed/native price (raw) */
|
|
49
|
+
export async function getSpeedEthPrice(chainId) {
|
|
50
|
+
const addr = ORACLE_ADDRESSES[chainId];
|
|
51
|
+
if (!addr)
|
|
52
|
+
throw new Error(`No oracle for chain ${chainId}`);
|
|
53
|
+
const provider = new ethers.JsonRpcProvider(getRpcUrl(chainId));
|
|
54
|
+
const contract = new ethers.Contract(addr, ORACLE_ABI, provider);
|
|
55
|
+
return contract.getLatestSpeedEthPrice();
|
|
56
|
+
}
|
|
57
|
+
/** All three oracle prices in one shot (one provider, parallel reads). Use for price command. */
|
|
58
|
+
export async function getOraclePrices(chainId) {
|
|
59
|
+
const addr = ORACLE_ADDRESSES[chainId];
|
|
60
|
+
if (!addr)
|
|
61
|
+
throw new Error(`No oracle for chain ${chainId}`);
|
|
62
|
+
const provider = new ethers.JsonRpcProvider(getRpcUrl(chainId));
|
|
63
|
+
const contract = new ethers.Contract(addr, ORACLE_ABI, provider);
|
|
64
|
+
const [ethUsd, speedUsd, speedEth] = await Promise.all([
|
|
65
|
+
contract.getEthUsdPrice(),
|
|
66
|
+
contract.getLatestSpeedUsdPrice(),
|
|
67
|
+
contract.getLatestSpeedEthPrice(),
|
|
68
|
+
]);
|
|
69
|
+
return { ethUsd, speedUsd, speedEth };
|
|
70
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a human token amount to wei (18 decimals).
|
|
3
|
+
* - Decimal (e.g. 0.1, 1.5) → token amount
|
|
4
|
+
* - Optional suffix "eth", "ether", "speed", "token" → token amount
|
|
5
|
+
* - Short integer (e.g. 10000, 1) → token amount
|
|
6
|
+
* - 18+ digit integer → raw wei (for power users)
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseTokenAmountToWei(amountRaw: string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a human token amount to wei (18 decimals).
|
|
4
|
+
* - Decimal (e.g. 0.1, 1.5) → token amount
|
|
5
|
+
* - Optional suffix "eth", "ether", "speed", "token" → token amount
|
|
6
|
+
* - Short integer (e.g. 10000, 1) → token amount
|
|
7
|
+
* - 18+ digit integer → raw wei (for power users)
|
|
8
|
+
*/
|
|
9
|
+
export function parseTokenAmountToWei(amountRaw) {
|
|
10
|
+
const trimmed = amountRaw.replace(/\s*(eth|ether|speed|token)\s*$/i, "").trim();
|
|
11
|
+
const hasDecimal = /\.\d/.test(amountRaw);
|
|
12
|
+
const hasSuffix = /\s*(eth|ether|speed|token)\s*$/i.test(amountRaw);
|
|
13
|
+
const allDigits = /^\d+$/.test(trimmed);
|
|
14
|
+
const looksLikeWei = allDigits && trimmed.length >= 18;
|
|
15
|
+
if (hasDecimal || hasSuffix || !looksLikeWei) {
|
|
16
|
+
return ethers.parseEther(trimmed).toString();
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PendingBridgeEntry {
|
|
2
|
+
requestId: string;
|
|
3
|
+
quoteId: string;
|
|
4
|
+
txHash: string;
|
|
5
|
+
fromChain: number;
|
|
6
|
+
toChain: number;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function addPendingBridge(entry: PendingBridgeEntry): void;
|
|
10
|
+
export declare function listPendingBridges(): PendingBridgeEntry[];
|
|
11
|
+
/** Remove entries by requestId; returns new list. */
|
|
12
|
+
export declare function removePendingBridges(requestIds: string[]): PendingBridgeEntry[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getSpeedConfigDir } from "../env.js";
|
|
4
|
+
const PENDING_BRIDGES_PATH = join(getSpeedConfigDir(), "pending-bridges.json");
|
|
5
|
+
function load() {
|
|
6
|
+
try {
|
|
7
|
+
if (existsSync(PENDING_BRIDGES_PATH)) {
|
|
8
|
+
const raw = readFileSync(PENDING_BRIDGES_PATH, "utf-8");
|
|
9
|
+
const data = JSON.parse(raw);
|
|
10
|
+
return Array.isArray(data) ? data : [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch (_) { }
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
function save(entries) {
|
|
17
|
+
mkdirSync(getSpeedConfigDir(), { recursive: true });
|
|
18
|
+
writeFileSync(PENDING_BRIDGES_PATH, JSON.stringify(entries, null, 2));
|
|
19
|
+
}
|
|
20
|
+
export function addPendingBridge(entry) {
|
|
21
|
+
const entries = load();
|
|
22
|
+
entries.unshift(entry);
|
|
23
|
+
save(entries);
|
|
24
|
+
}
|
|
25
|
+
export function listPendingBridges() {
|
|
26
|
+
return load();
|
|
27
|
+
}
|
|
28
|
+
/** Remove entries by requestId; returns new list. */
|
|
29
|
+
export function removePendingBridges(requestIds) {
|
|
30
|
+
const set = new Set(requestIds);
|
|
31
|
+
const entries = load().filter((e) => !set.has(e.requestId));
|
|
32
|
+
save(entries);
|
|
33
|
+
return entries;
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ISquid {
|
|
2
|
+
getRoute(params: SquidRouteParams): Promise<{
|
|
3
|
+
route: unknown;
|
|
4
|
+
requestId: string;
|
|
5
|
+
}>;
|
|
6
|
+
executeRoute(opts: {
|
|
7
|
+
signer: unknown;
|
|
8
|
+
route: unknown;
|
|
9
|
+
}): Promise<unknown>;
|
|
10
|
+
getStatus(params: {
|
|
11
|
+
transactionId: string;
|
|
12
|
+
requestId: string;
|
|
13
|
+
integratorId: string;
|
|
14
|
+
quoteId: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
squidTransactionStatus: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export declare function getSquid(): Promise<ISquid>;
|
|
20
|
+
export interface SquidRouteParams {
|
|
21
|
+
fromAddress: string;
|
|
22
|
+
fromChain: string;
|
|
23
|
+
fromToken: string;
|
|
24
|
+
fromAmount: string;
|
|
25
|
+
toChain: string;
|
|
26
|
+
toToken: string;
|
|
27
|
+
toAddress: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SQUID_BASE_URL } from "../constants.js";
|
|
2
|
+
let squidInstance = null;
|
|
3
|
+
function getIntegratorId() {
|
|
4
|
+
const id = process.env.SQUID_INTEGRATOR_ID?.trim();
|
|
5
|
+
if (!id)
|
|
6
|
+
throw new Error("SQUID_INTEGRATOR_ID must be set");
|
|
7
|
+
return id;
|
|
8
|
+
}
|
|
9
|
+
async function initSquid() {
|
|
10
|
+
const { Squid } = await import("@0xsquid/sdk");
|
|
11
|
+
const squid = new Squid({
|
|
12
|
+
baseUrl: SQUID_BASE_URL,
|
|
13
|
+
integratorId: getIntegratorId(),
|
|
14
|
+
});
|
|
15
|
+
await squid.init();
|
|
16
|
+
return squid;
|
|
17
|
+
}
|
|
18
|
+
export async function getSquid() {
|
|
19
|
+
if (squidInstance)
|
|
20
|
+
return squidInstance;
|
|
21
|
+
squidInstance = await initSquid();
|
|
22
|
+
return squidInstance;
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { type ZeroXQuoteResult } from "./zerox.js";
|
|
3
|
+
/**
|
|
4
|
+
* Execute swap from an existing quote: approve if needed → send transaction → wait.
|
|
5
|
+
* Used by swap command after it has already fetched the quote for display/prechecks.
|
|
6
|
+
*/
|
|
7
|
+
export declare function executeSwapWithQuote(signer: ethers.Signer, sellToken: string, quote: ZeroXQuoteResult, amountWei: string): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Execute one swap: get quote → approve if needed → send transaction → wait.
|
|
10
|
+
* Used by volume command. No confirmation, no dry-run.
|
|
11
|
+
* @returns { txHash, quote }
|
|
12
|
+
*/
|
|
13
|
+
export declare function executeSwap(chainId: number, signer: ethers.Signer, sellToken: string, buyToken: string, amountWei: string): Promise<{
|
|
14
|
+
txHash: string;
|
|
15
|
+
quote: ZeroXQuoteResult;
|
|
16
|
+
}>;
|