@lifi/cli 0.1.1-alpha.1

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/lifi.js +622 -0
  4. package/package.json +76 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LI.FI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # LI.FI CLI
2
+
3
+ > **Note:** This CLI provides **read-only** tools — it does not sign or broadcast transactions. Quote responses include unsigned `transactionRequest` objects that must be signed and submitted externally using your own wallet.
4
+
5
+ A TypeScript CLI that wraps the [LI.FI REST API](https://li.quest) to give developers, integrators, and internal teams a scriptable, human-readable interface to cross-chain swap infrastructure.
6
+
7
+ ## Quickstart
8
+
9
+ ```bash
10
+ git clone https://github.com/lifinance/lifi-cli.git
11
+ cd lifi-cli
12
+ npm install && npm run build
13
+ node dist/lifi.cjs chains
14
+ ```
15
+
16
+ Once published to npm:
17
+
18
+ ```bash
19
+ npm install -g @lifi/cli
20
+ lifi chains
21
+ ```
22
+
23
+ No API key required — works immediately with public rate limits. Add a key for higher throughput.
24
+
25
+ ## Commands
26
+
27
+ ### Token Information
28
+
29
+ ```bash
30
+ lifi tokens --chain 1 # List tokens on Ethereum
31
+ lifi tokens --chain 1 --min-price 100 # Filter by min USD price
32
+ lifi token 1 USDC # Get specific token detail
33
+ lifi token 1 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 # By address
34
+ ```
35
+
36
+ ### Chain Information
37
+
38
+ ```bash
39
+ lifi chains # List all supported chains
40
+ lifi chains --type EVM # Filter by chain type
41
+ lifi chain 42161 # Get chain detail by ID
42
+ lifi chain arbitrum # Get chain detail by name (case-insensitive)
43
+ ```
44
+
45
+ ### Quote & Swap
46
+
47
+ ```bash
48
+ # With flags
49
+ lifi quote \
50
+ --from ethereum --to arbitrum \
51
+ --from-token USDC --to-token USDC \
52
+ --amount 1000000000 \
53
+ --from-address 0xd8dA...
54
+
55
+ # Interactive mode (prompts for missing flags)
56
+ lifi quote
57
+ ```
58
+
59
+ ### Routes
60
+
61
+ ```bash
62
+ lifi routes \
63
+ --from 1 --to 42161 \
64
+ --from-token USDC --to-token USDC \
65
+ --amount 1000000000 \
66
+ --order CHEAPEST
67
+ ```
68
+
69
+ ### Transaction Status
70
+
71
+ ```bash
72
+ lifi status 0xabc123... # One-shot status check
73
+ lifi status 0xabc123... --watch # Poll until complete/failed
74
+ lifi status 0xabc123... --bridge hop # Speed up lookup with bridge hint
75
+ ```
76
+
77
+ ### Connections
78
+
79
+ ```bash
80
+ lifi connections # All connections
81
+ lifi connections --from-chain 1 --to-chain 42161 # Specific pair
82
+ ```
83
+
84
+ ### Tools (Bridges & DEXes)
85
+
86
+ ```bash
87
+ lifi tools # List all bridges and DEXes
88
+ lifi tools --chain 1 # Filter by chain
89
+ ```
90
+
91
+ ### Gas
92
+
93
+ ```bash
94
+ lifi gas # Gas prices for all chains
95
+ lifi gas 1 # Detailed gas suggestion for Ethereum
96
+ ```
97
+
98
+ ### API Key Management
99
+
100
+ ```bash
101
+ lifi auth show # Display masked key
102
+ lifi auth test # Validate key against the API
103
+ ```
104
+
105
+ ### Health Check
106
+
107
+ ```bash
108
+ lifi health # Check API connectivity and latency
109
+ ```
110
+
111
+ ## Output Modes
112
+
113
+ | Mode | Trigger | Behaviour |
114
+ |------|---------|-----------|
115
+ | Human | Default (TTY detected) | Coloured tables, formatted amounts |
116
+ | Machine | `--json` flag or non-TTY pipe | Raw JSON, stable schema, no colour |
117
+
118
+ ```bash
119
+ # Pipe-friendly — auto-detects non-TTY
120
+ lifi chains | jq '.chains[].name'
121
+
122
+ # Force JSON in terminal
123
+ lifi chains --json
124
+
125
+ # Disable colour
126
+ lifi chains --no-color
127
+
128
+ # Verbose errors with stack traces
129
+ lifi quote --from 1 --to 42161 --verbose
130
+ ```
131
+
132
+ ## Configuration
133
+
134
+ All configuration is via environment variables. No config files needed.
135
+
136
+ ```bash
137
+ # LI.FI API key (higher rate limits)
138
+ export LIFI_API_KEY=your_key_here
139
+ ```
140
+
141
+ Without `LIFI_API_KEY`, the CLI uses public rate limits (200 req/2hr). With a key, you get 200 req/min.
142
+
143
+ ## Exit Codes
144
+
145
+ | Code | Meaning |
146
+ |------|---------|
147
+ | 0 | Success |
148
+ | 1 | General error |
149
+ | 2 | Invalid arguments / usage |
150
+ | 3 | Authentication error |
151
+ | 4 | API error (rate limit, server error) |
152
+ | 5 | Network error (unreachable) |
153
+
154
+ ## Common Chain IDs
155
+
156
+ | Chain | ID | Native Token |
157
+ |-------|-----|--------------|
158
+ | Ethereum | 1 | ETH |
159
+ | Polygon | 137 | MATIC |
160
+ | Arbitrum | 42161 | ETH |
161
+ | Optimism | 10 | ETH |
162
+ | BSC | 56 | BNB |
163
+ | Avalanche | 43114 | AVAX |
164
+ | Base | 8453 | ETH |
165
+
166
+ ## Example Workflow: Cross-Chain Swap
167
+
168
+ ```bash
169
+ # 1. Find chain IDs
170
+ lifi chains --type EVM
171
+
172
+ # 2. Look up token addresses
173
+ lifi token 1 USDC
174
+
175
+ # 3. Get best quote
176
+ lifi quote --from 1 --to 8453 --from-token USDC --to-token USDC --amount 1000000000 --from-address 0xYOUR_ADDRESS
177
+
178
+ # 4. (External) Approve tokens and sign transactionRequest with your wallet
179
+
180
+ # 5. Track progress
181
+ lifi status 0xTX_HASH --watch
182
+ ```
183
+
184
+ ## Installation
185
+
186
+ ### npm (primary)
187
+
188
+ ```bash
189
+ npm install -g @lifi/cli
190
+ ```
191
+
192
+ ### npx (no install)
193
+
194
+ ```bash
195
+ npx @lifi/cli chains
196
+ ```
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ git clone https://github.com/lifinance/lifi-cli.git
202
+ cd lifi-cli
203
+ npm install
204
+ npm run build
205
+ node dist/lifi.cjs --help
206
+ ```
207
+
208
+ ### Scripts
209
+
210
+ | Script | Description |
211
+ |--------|-------------|
212
+ | `npm run build` | Build with tsup |
213
+ | `npm run dev` | Watch mode |
214
+ | `npm test` | Run tests (vitest) |
215
+ | `npm run typecheck` | Type check (tsc --noEmit) |
216
+
217
+ ## License
218
+
219
+ MIT
package/dist/lifi.js ADDED
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import Table from "cli-table3";
4
+ import axios from "axios";
5
+ import ora from "ora";
6
+ import { input } from "@inquirer/prompts";
7
+ //#region src/core/constants.ts
8
+ const API_BASE_URL = "https://li.quest/v1";
9
+ const INTEGRATOR_ID = "lifi-cli";
10
+ const AUTH_HEADER = "X-LiFi-Api-Key";
11
+ let ExitCode = /* @__PURE__ */ function(ExitCode) {
12
+ ExitCode[ExitCode["Success"] = 0] = "Success";
13
+ ExitCode[ExitCode["General"] = 1] = "General";
14
+ ExitCode[ExitCode["InvalidArgs"] = 2] = "InvalidArgs";
15
+ ExitCode[ExitCode["AuthError"] = 3] = "AuthError";
16
+ ExitCode[ExitCode["ApiError"] = 4] = "ApiError";
17
+ ExitCode[ExitCode["NetworkError"] = 5] = "NetworkError";
18
+ return ExitCode;
19
+ }({});
20
+ //#endregion
21
+ //#region src/core/config.ts
22
+ function getApiKey() {
23
+ return process.env["LIFI_API_KEY"] || void 0;
24
+ }
25
+ function maskKey(key) {
26
+ if (!key) return "";
27
+ if (key.length <= 6) return "***";
28
+ return `${key.slice(0, 3)}...${key.slice(-3)}`;
29
+ }
30
+ //#endregion
31
+ //#region src/core/errors.ts
32
+ var CliError = class extends Error {
33
+ exitCode;
34
+ hint;
35
+ constructor(message, exitCode = ExitCode.General, hint) {
36
+ super(message);
37
+ this.name = "CliError";
38
+ this.exitCode = exitCode;
39
+ this.hint = hint;
40
+ }
41
+ };
42
+ function formatError(error) {
43
+ if (error instanceof CliError) {
44
+ const lines = [`\u2717 Error: ${error.message}`];
45
+ if (error.hint) lines.push("", ` ${error.hint}`);
46
+ return lines.join("\n");
47
+ }
48
+ if (error instanceof Error) return `\u2717 Error: ${error.message}`;
49
+ return "✗ Error: An unexpected error occurred";
50
+ }
51
+ function mapAxiosError(error) {
52
+ const response = error.response;
53
+ if (response) {
54
+ const msg = response.data?.message || `HTTP ${response.status}`;
55
+ if (response.status === 401 || response.status === 403) return new CliError(msg, ExitCode.AuthError, "Check your API key with: lifi auth test");
56
+ if (response.status === 429) return new CliError(msg, ExitCode.ApiError, "Rate limited. Set an API key with: lifi auth set <key>");
57
+ if (response.status >= 400) return new CliError(msg, ExitCode.ApiError);
58
+ }
59
+ if (error.code === "ECONNREFUSED" || error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") return new CliError(error.message || "Network error", ExitCode.NetworkError, "Check your internet connection and try again.");
60
+ return new CliError(error.message || "An unexpected error occurred", ExitCode.General);
61
+ }
62
+ function handleError(error) {
63
+ const verbose = process.env["LIFI_VERBOSE"] === "1";
64
+ if (error instanceof CliError) {
65
+ console.error(formatError(error));
66
+ if (verbose && error.stack) console.error(`\n${error.stack}`);
67
+ process.exit(error.exitCode);
68
+ }
69
+ console.error(formatError(error));
70
+ if (verbose && error instanceof Error && error.stack) console.error(`\n${error.stack}`);
71
+ process.exit(ExitCode.General);
72
+ }
73
+ //#endregion
74
+ //#region src/core/formatter.ts
75
+ function formatAmount(amount, decimals) {
76
+ if (amount === "0") return "0";
77
+ const padded = amount.padStart(decimals + 1, "0");
78
+ const intPart = padded.slice(0, padded.length - decimals) || "0";
79
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
80
+ if (!fracPart) return intPart;
81
+ return `${intPart}.${fracPart}`;
82
+ }
83
+ function formatTable(headers, rows) {
84
+ const table = new Table({ head: headers });
85
+ for (const row of rows) table.push(row);
86
+ return table.toString();
87
+ }
88
+ function isJsonMode(options) {
89
+ if (options.json === true) return true;
90
+ return !process.stdout.isTTY;
91
+ }
92
+ function jsonOutput(data) {
93
+ return JSON.stringify(data, null, 2);
94
+ }
95
+ //#endregion
96
+ //#region src/core/http-client.ts
97
+ function createApiClient() {
98
+ const client = axios.create({
99
+ baseURL: API_BASE_URL,
100
+ timeout: 3e4
101
+ });
102
+ client.interceptors.request.use((config) => {
103
+ const apiKey = getApiKey();
104
+ if (apiKey) config.headers[AUTH_HEADER] = apiKey;
105
+ config.params = config.params || {};
106
+ config.params.integrator = INTEGRATOR_ID;
107
+ return config;
108
+ });
109
+ client.interceptors.response.use((response) => response, (error) => {
110
+ throw mapAxiosError(error);
111
+ });
112
+ return client;
113
+ }
114
+ const api = createApiClient();
115
+ //#endregion
116
+ //#region src/core/interactive.ts
117
+ async function withSpinner(message, fn) {
118
+ const spinner = ora(message).start();
119
+ try {
120
+ const result = await fn();
121
+ spinner.succeed();
122
+ return result;
123
+ } catch (error) {
124
+ spinner.fail();
125
+ throw error;
126
+ }
127
+ }
128
+ //#endregion
129
+ //#region src/commands/auth.ts
130
+ function registerAuthCommand(program) {
131
+ const auth = program.command("auth").description("Manage API key (set via LIFI_API_KEY env var)");
132
+ auth.command("show").description("Display current API key (masked) and its source").action(async (_options, command) => {
133
+ const opts = command.optsWithGlobals();
134
+ try {
135
+ const key = getApiKey();
136
+ if (!key) {
137
+ console.log("No API key configured.");
138
+ console.log("Set one with: export LIFI_API_KEY=<your-key>");
139
+ return;
140
+ }
141
+ if (isJsonMode(opts)) console.log(jsonOutput({ key: maskKey(key) }));
142
+ else {
143
+ console.log(`API Key: ${maskKey(key)}`);
144
+ console.log("Source: LIFI_API_KEY env var");
145
+ }
146
+ } catch (error) {
147
+ handleError(error);
148
+ }
149
+ });
150
+ auth.command("test").description("Validate API key against the LI.FI API").action(async (_options, command) => {
151
+ const opts = command.optsWithGlobals();
152
+ try {
153
+ const { data } = await withSpinner("Testing API key...", () => api.get("/keys/test"));
154
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
155
+ else console.log("API key is valid.");
156
+ } catch (error) {
157
+ handleError(error);
158
+ }
159
+ });
160
+ }
161
+ //#endregion
162
+ //#region src/commands/chains.ts
163
+ function registerChainsCommand(program) {
164
+ program.command("chains").description("List all supported blockchain networks").addOption(new Option("--type <type>", "Filter by chain type").choices(["EVM", "SVM"])).addHelpText("after", `
165
+ Examples:
166
+ $ lifi chains # All chains
167
+ $ lifi chains --type EVM # Only EVM chains
168
+ $ lifi chains --json | jq '.chains[] | {id, name}'`).action(async (options, command) => {
169
+ const opts = command.optsWithGlobals();
170
+ try {
171
+ const { data } = await withSpinner("Fetching chains...", () => api.get("/chains"));
172
+ let chains = data.chains;
173
+ if (options.type) {
174
+ const type = options.type;
175
+ chains = chains.filter((c) => c.chainType === type);
176
+ }
177
+ if (isJsonMode(opts)) console.log(jsonOutput({ chains }));
178
+ else {
179
+ const rows = chains.map((c) => [
180
+ String(c.id),
181
+ c.name,
182
+ c.chainType,
183
+ c.nativeToken.symbol
184
+ ]);
185
+ console.log(formatTable([
186
+ "ID",
187
+ "Name",
188
+ "Type",
189
+ "Native Token"
190
+ ], rows));
191
+ console.log("\n Next: lifi chain <id> or lifi tokens --chain <id>");
192
+ }
193
+ } catch (error) {
194
+ handleError(error);
195
+ }
196
+ });
197
+ program.command("chain <idOrName>").description("Look up a single chain by numeric ID or name (case-insensitive)").addHelpText("after", `
198
+ Examples:
199
+ $ lifi chain 42161 # By ID
200
+ $ lifi chain arbitrum # By name
201
+ $ lifi chain ethereum --json # JSON output`).action(async (idOrName, _options, command) => {
202
+ const opts = command.optsWithGlobals();
203
+ try {
204
+ const { data } = await withSpinner("Fetching chains...", () => api.get("/chains"));
205
+ const allChains = data.chains;
206
+ const chain = /^\d+$/.test(idOrName) ? allChains.find((c) => c.id === Number(idOrName)) : allChains.find((c) => c.name.toLowerCase() === idOrName.toLowerCase());
207
+ if (!chain) {
208
+ handleError(new CliError(`Chain "${idOrName}" not found`, ExitCode.InvalidArgs, "Run: lifi chains to see all available chains"));
209
+ return;
210
+ }
211
+ if (isJsonMode(opts)) console.log(jsonOutput(chain));
212
+ else {
213
+ const rows = [
214
+ ["ID", String(chain.id)],
215
+ ["Name", chain.name],
216
+ ["Key", chain.key],
217
+ ["Type", chain.chainType],
218
+ ["Native Token", chain.nativeToken.symbol],
219
+ ["Mainnet", chain.mainnet ? "Yes" : "No"]
220
+ ];
221
+ console.log(formatTable(["Field", "Value"], rows));
222
+ }
223
+ } catch (error) {
224
+ handleError(error);
225
+ }
226
+ });
227
+ }
228
+ //#endregion
229
+ //#region src/commands/connections.ts
230
+ function registerConnectionsCommand(program) {
231
+ program.command("connections").description("Check which token transfer routes exist between chains").option("--from-chain <chainId>", "Source chain ID (e.g. 1)").option("--to-chain <chainId>", "Destination chain ID (e.g. 42161)").option("--from-token <token>", "Source token address (e.g. 0xa0b8...)").option("--to-token <token>", "Destination token address").addHelpText("after", `
232
+ Examples:
233
+ $ lifi connections --from-chain 1 --to-chain 42161
234
+ $ lifi connections --from-chain 1 --to-chain 8453 --from-token USDC --json`).action(async (options, command) => {
235
+ const opts = command.optsWithGlobals();
236
+ try {
237
+ const params = {};
238
+ if (options["fromChain"]) params["fromChain"] = options["fromChain"];
239
+ if (options["toChain"]) params["toChain"] = options["toChain"];
240
+ if (options["fromToken"]) params["fromToken"] = options["fromToken"];
241
+ if (options["toToken"]) params["toToken"] = options["toToken"];
242
+ const { data } = await withSpinner("Fetching connections...", () => api.get("/connections", { params }));
243
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
244
+ else {
245
+ const connections = data.connections ?? [];
246
+ if (connections.length === 0) {
247
+ console.log("No connections found for the given filters.");
248
+ return;
249
+ }
250
+ const rows = connections.slice(0, 50).map((c) => [
251
+ String(c.fromChainId),
252
+ String(c.toChainId),
253
+ c.fromToken?.symbol ?? "-",
254
+ c.toToken?.symbol ?? "-"
255
+ ]);
256
+ console.log(formatTable([
257
+ "From Chain",
258
+ "To Chain",
259
+ "From Token",
260
+ "To Token"
261
+ ], rows));
262
+ if (connections.length > 50) console.log(`\n Showing 50 of ${connections.length} connections. Use --json for full list.`);
263
+ }
264
+ } catch (error) {
265
+ handleError(error);
266
+ }
267
+ });
268
+ }
269
+ //#endregion
270
+ //#region src/commands/gas.ts
271
+ function registerGasCommand(program) {
272
+ program.command("gas [chain]").description("Get gas prices for all chains, or detailed suggestion for one chain").addHelpText("after", `
273
+ Examples:
274
+ $ lifi gas # All chains gas prices
275
+ $ lifi gas 1 # Ethereum gas suggestion (recommended cost in USD)
276
+ $ lifi gas ethereum --json`).action(async (chain, _options, command) => {
277
+ const opts = command.optsWithGlobals();
278
+ try {
279
+ if (chain) {
280
+ const { data } = await withSpinner(`Fetching gas for chain ${chain}...`, () => api.get(`/gas/suggestion/${chain}`));
281
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
282
+ else {
283
+ const rec = data.recommended;
284
+ const rows = [
285
+ ["Token", rec.token?.symbol || "N/A"],
286
+ ["Recommended cost", rec.amountUsd ? `$${rec.amountUsd}` : "N/A"],
287
+ ["Available", data.available ? "Yes" : "No"]
288
+ ];
289
+ console.log(formatTable(["Field", "Value"], rows));
290
+ }
291
+ } else {
292
+ const { data } = await withSpinner("Fetching gas prices...", () => api.get("/gas/prices"));
293
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
294
+ else {
295
+ const rows = Object.keys(data).slice(0, 30).map((id) => {
296
+ const g = data[id];
297
+ return [
298
+ id,
299
+ String(g?.standard ?? "-"),
300
+ String(g?.fast ?? "-"),
301
+ String(g?.fastest ?? "-")
302
+ ];
303
+ });
304
+ console.log(formatTable([
305
+ "Chain ID",
306
+ "Standard (gwei)",
307
+ "Fast",
308
+ "Fastest"
309
+ ], rows));
310
+ if (Object.keys(data).length > 30) console.log(`\n Showing 30 of ${Object.keys(data).length} chains. Use --json for full list.`);
311
+ }
312
+ }
313
+ } catch (error) {
314
+ handleError(error);
315
+ }
316
+ });
317
+ }
318
+ //#endregion
319
+ //#region src/commands/health.ts
320
+ function registerHealthCommand(program) {
321
+ program.command("health").description("Check LI.FI API connectivity, latency, and available chains").action(async (_options, command) => {
322
+ const opts = command.optsWithGlobals();
323
+ try {
324
+ const start = Date.now();
325
+ const { data } = await withSpinner("Checking API health...", () => api.get("/chains"));
326
+ const latency = Date.now() - start;
327
+ const chains = data.chains || data;
328
+ const result = {
329
+ status: "ok",
330
+ latencyMs: latency,
331
+ chainsAvailable: Array.isArray(chains) ? chains.length : 0
332
+ };
333
+ if (isJsonMode(opts)) console.log(jsonOutput(result));
334
+ else console.log(`LI.FI API: ok (${latency}ms, ${result.chainsAvailable} chains available)`);
335
+ } catch (error) {
336
+ handleError(error);
337
+ }
338
+ });
339
+ }
340
+ //#endregion
341
+ //#region src/commands/quote.ts
342
+ async function promptIfMissing(value, label) {
343
+ if (value) return value;
344
+ if (process.env["LIFI_NO_INPUT"] === "1") throw new CliError(`Missing required option: ${label}`, ExitCode.InvalidArgs, "Pass all required flags when using --no-input");
345
+ return input({ message: `${label}:` });
346
+ }
347
+ function registerQuoteCommand(program) {
348
+ program.command("quote").description("Get the best route for a cross-chain or same-chain swap").option("--from <chain>", "Source chain name or ID (e.g. ethereum, 1)").option("--to <chain>", "Destination chain name or ID (e.g. arbitrum, 42161)").option("--from-token <token>", "Token to send — symbol or address (e.g. USDC, 0xa0b8...)").option("--to-token <token>", "Token to receive — symbol or address").option("--amount <amount>", "Amount in smallest unit (e.g. 1000000 for 1 USDC)").option("--from-address <address>", "Sender wallet address (0x...)").option("--slippage <slippage>", "Max slippage as decimal (e.g. 0.03 for 3%)", "0.03").addOption(new Option("--order <order>", "Route preference").choices([
349
+ "CHEAPEST",
350
+ "FASTEST",
351
+ "SAFEST",
352
+ "RECOMMENDED"
353
+ ])).option("--allow-bridges <keys>", "Only use these bridges (comma-separated keys from lifi tools)").option("--allow-exchanges <keys>", "Only use these exchanges (comma-separated keys from lifi tools)").addHelpText("after", `
354
+ Examples:
355
+ $ lifi quote --from ethereum --to arbitrum --from-token USDC --to-token USDC --amount 1000000 --from-address 0xd8dA...
356
+ $ lifi quote # Interactive mode — prompts for each field
357
+ $ lifi quote --from 1 --to 8453 --from-token USDC --to-token USDC --amount 1000000000 --json`).action(async (options, command) => {
358
+ const opts = command.optsWithGlobals();
359
+ try {
360
+ const params = {
361
+ fromChain: await promptIfMissing(options.from, "--from (source chain)"),
362
+ toChain: await promptIfMissing(options.to, "--to (destination chain)"),
363
+ fromToken: await promptIfMissing(options.fromToken, "--from-token"),
364
+ toToken: await promptIfMissing(options.toToken, "--to-token"),
365
+ fromAmount: await promptIfMissing(options.amount, "--amount"),
366
+ fromAddress: await promptIfMissing(options.fromAddress, "--from-address"),
367
+ slippage: options.slippage
368
+ };
369
+ if (options["order"]) params["order"] = options["order"];
370
+ if (options["allowBridges"]) params["allowBridges"] = options["allowBridges"];
371
+ if (options["allowExchanges"]) params["allowExchanges"] = options["allowExchanges"];
372
+ const { data } = await withSpinner("Fetching quote...", () => api.get("/quote", { params }));
373
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
374
+ else {
375
+ const estimate = data.estimate;
376
+ const rows = [
377
+ ["You receive", `${formatAmount(estimate?.toAmount ?? "0", estimate?.toAmountDecimals ?? 18)} ${data.action?.toToken?.symbol ?? ""}`],
378
+ ["Bridge", data.toolDetails?.name ?? data.tool ?? "N/A"],
379
+ ["Est. time", estimate?.executionDuration ? `~${Math.round(estimate.executionDuration / 60)} min` : "N/A"],
380
+ ["Gas cost", estimate?.gasCosts?.[0]?.amountUSD ? `~$${estimate.gasCosts[0].amountUSD}` : "N/A"],
381
+ ["Slippage", `${Number(options["slippage"]) * 100}%`]
382
+ ];
383
+ console.log(formatTable(["", ""], rows));
384
+ console.log("\n Sign the transactionRequest with your wallet to execute.");
385
+ console.log(" Then track with: lifi status <txHash> --watch");
386
+ console.log(" Use --json to get the full transactionRequest object.");
387
+ }
388
+ } catch (error) {
389
+ handleError(error);
390
+ }
391
+ });
392
+ }
393
+ //#endregion
394
+ //#region src/commands/routes.ts
395
+ function registerRoutesCommand(program) {
396
+ program.command("routes").description("Get multiple route options for comparison (unlike quote, returns several alternatives)").option("--from <chain>", "Source chain name or ID (e.g. ethereum, 1)").option("--to <chain>", "Destination chain name or ID (e.g. arbitrum, 42161)").option("--from-token <token>", "Token to send — symbol or address (e.g. USDC)").option("--to-token <token>", "Token to receive — symbol or address").option("--amount <amount>", "Amount in smallest unit (e.g. 1000000 for 1 USDC)").option("--from-address <address>", "Sender wallet address (0x...)").addOption(new Option("--order <order>", "Sort preference").choices([
397
+ "CHEAPEST",
398
+ "FASTEST",
399
+ "SAFEST",
400
+ "RECOMMENDED"
401
+ ])).addHelpText("after", `
402
+ Examples:
403
+ $ lifi routes --from 1 --to 42161 --from-token USDC --to-token USDC --amount 1000000000 --json
404
+ $ lifi routes --from ethereum --to base --from-token USDC --to-token USDC --amount 1000000 --order CHEAPEST`).action(async (options, command) => {
405
+ const opts = command.optsWithGlobals();
406
+ try {
407
+ const body = {
408
+ fromChainId: options.from,
409
+ toChainId: options.to,
410
+ fromTokenAddress: options.fromToken,
411
+ toTokenAddress: options.toToken,
412
+ fromAmount: options.amount,
413
+ fromAddress: options.fromAddress || "0x0000000000000000000000000000000000000000",
414
+ options: options.order ? { order: options.order } : void 0
415
+ };
416
+ const { data } = await withSpinner("Fetching routes...", () => api.post("/advanced/routes", body));
417
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
418
+ else {
419
+ const rows = (data.routes ?? []).map((r, i) => [
420
+ String(i + 1),
421
+ r.steps.map((s) => s.tool).join(" → "),
422
+ r.toAmountUSD ? `$${r.toAmountUSD}` : "N/A",
423
+ r.gasCostUSD ? `$${r.gasCostUSD}` : "N/A"
424
+ ]);
425
+ console.log(formatTable([
426
+ "#",
427
+ "Steps",
428
+ "You Receive (USD)",
429
+ "Gas Cost"
430
+ ], rows));
431
+ }
432
+ } catch (error) {
433
+ handleError(error);
434
+ }
435
+ });
436
+ }
437
+ //#endregion
438
+ //#region src/types/index.ts
439
+ const TERMINAL_STATUSES = new Set([
440
+ "DONE",
441
+ "FAILED",
442
+ "CANCELLED",
443
+ "NOT_FOUND",
444
+ "INVALID"
445
+ ]);
446
+ //#endregion
447
+ //#region src/commands/status.ts
448
+ const MAX_POLL_ATTEMPTS = 60;
449
+ function registerStatusCommand(program) {
450
+ program.command("status <txHash>").description("Check cross-chain transfer status by transaction hash").option("--bridge <bridge>", "Bridge key hint to speed up lookup (e.g. stargate, hop, across)").option("--from-chain <chainId>", "Source chain ID (e.g. 1)").option("--to-chain <chainId>", "Destination chain ID (e.g. 42161)").option("--watch", "Poll every 5s until DONE or FAILED (max 5 min)").addHelpText("after", `
451
+ Examples:
452
+ $ lifi status 0xabc123def456...
453
+ $ lifi status 0xabc123... --watch
454
+ $ lifi status 0xabc123... --bridge stargate --from-chain 1 --to-chain 42161`).action(async (txHash, options, command) => {
455
+ const opts = command.optsWithGlobals();
456
+ try {
457
+ const params = { txHash };
458
+ if (options["bridge"]) params["bridge"] = options["bridge"];
459
+ if (options["fromChain"]) params["fromChain"] = options["fromChain"];
460
+ if (options["toChain"]) params["toChain"] = options["toChain"];
461
+ const fetchStatus = () => api.get("/status", { params });
462
+ if (options.watch) {
463
+ let lastData = null;
464
+ let status = "PENDING";
465
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
466
+ const data = (await withSpinner(`Status: ${status} (${attempt + 1}/${MAX_POLL_ATTEMPTS})`, fetchStatus)).data;
467
+ status = data.status ?? "UNKNOWN";
468
+ lastData = data;
469
+ if (!isJsonMode(opts)) console.log(`Status: ${status} | Substatus: ${data.substatus ?? "N/A"}`);
470
+ if (TERMINAL_STATUSES.has(status)) break;
471
+ await new Promise((r) => setTimeout(r, 5e3));
472
+ }
473
+ if (isJsonMode(opts)) console.log(jsonOutput(lastData));
474
+ if (!TERMINAL_STATUSES.has(status)) throw new CliError(`Polling timed out after ${MAX_POLL_ATTEMPTS * 5}s — last status: ${status}`, ExitCode.General, `Try again with: lifi status ${txHash} --watch`);
475
+ } else {
476
+ const { data } = await withSpinner("Checking status...", fetchStatus);
477
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
478
+ else {
479
+ const rows = [
480
+ ["Status", data.status ?? "N/A"],
481
+ ["Substatus", data.substatus ?? "N/A"],
482
+ ["Bridge", data.tool ?? "N/A"]
483
+ ];
484
+ console.log(formatTable(["Field", "Value"], rows));
485
+ }
486
+ }
487
+ } catch (error) {
488
+ handleError(error);
489
+ }
490
+ });
491
+ }
492
+ //#endregion
493
+ //#region src/commands/tokens.ts
494
+ function registerTokensCommand(program) {
495
+ program.command("tokens").description("List supported tokens across chains").option("--chain <chainId>", "Filter by chain ID (e.g. 1 for Ethereum, 137 for Polygon)").option("--min-price <price>", "Only show tokens with USD price >= this value").addHelpText("after", `
496
+ Examples:
497
+ $ lifi tokens --chain 1 # All Ethereum tokens
498
+ $ lifi tokens --chain 1 --min-price 100 # Tokens worth $100+
499
+ $ lifi tokens --chain 1 --json | jq '.tokens["1"][] | .symbol'`).action(async (options, command) => {
500
+ const opts = command.optsWithGlobals();
501
+ try {
502
+ const params = {};
503
+ if (options["chain"]) params["chains"] = options["chain"];
504
+ const { data } = await withSpinner("Fetching tokens...", () => api.get("/tokens", { params }));
505
+ if (options["minPrice"]) {
506
+ const minPrice = Number(options["minPrice"]);
507
+ const tokens = data.tokens;
508
+ for (const chainId of Object.keys(tokens)) {
509
+ const chainTokens = tokens[chainId];
510
+ if (chainTokens) tokens[chainId] = chainTokens.filter((t) => Number(t.priceUSD ?? 0) >= minPrice);
511
+ }
512
+ }
513
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
514
+ else {
515
+ const tokens = data.tokens;
516
+ const allTokens = [];
517
+ for (const chainId of Object.keys(tokens)) {
518
+ const chainTokens = tokens[chainId];
519
+ if (chainTokens) allTokens.push(...chainTokens);
520
+ }
521
+ const rows = allTokens.slice(0, 50).map((t) => [
522
+ t.symbol,
523
+ t.name,
524
+ `${t.address.slice(0, 10)}...`,
525
+ String(t.decimals),
526
+ String(t.chainId)
527
+ ]);
528
+ console.log(formatTable([
529
+ "Symbol",
530
+ "Name",
531
+ "Address",
532
+ "Decimals",
533
+ "Chain"
534
+ ], rows));
535
+ if (allTokens.length > 50) console.log(`\n Showing 50 of ${allTokens.length} tokens. Use --json for full list.`);
536
+ console.log("\n Next: lifi token <chain> <symbol> or lifi quote --from-token <symbol>");
537
+ }
538
+ } catch (error) {
539
+ handleError(error);
540
+ }
541
+ });
542
+ program.command("token <chain> <symbol>").description("Get details for a specific token by chain and symbol or address").addHelpText("after", `
543
+ Examples:
544
+ $ lifi token 1 USDC # By symbol
545
+ $ lifi token ethereum USDC # By chain name
546
+ $ lifi token 1 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # By address`).action(async (chain, symbol, _options, command) => {
547
+ const opts = command.optsWithGlobals();
548
+ try {
549
+ const { data } = await withSpinner("Fetching token...", () => api.get("/token", { params: {
550
+ chain,
551
+ token: symbol
552
+ } }));
553
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
554
+ else {
555
+ const rows = [
556
+ ["Symbol", data.symbol],
557
+ ["Name", data.name],
558
+ ["Address", data.address],
559
+ ["Decimals", String(data.decimals)],
560
+ ["Chain ID", String(data.chainId)],
561
+ ["Price (USD)", data.priceUSD ?? "N/A"]
562
+ ];
563
+ console.log(formatTable(["Field", "Value"], rows));
564
+ }
565
+ } catch (error) {
566
+ handleError(error);
567
+ }
568
+ });
569
+ }
570
+ //#endregion
571
+ //#region src/commands/tools.ts
572
+ function registerToolsCommand(program) {
573
+ program.command("tools").description("List available bridges and DEX aggregators — use keys in quote --allow-bridges/--allow-exchanges").option("--chain <chainId>", "Filter by chain ID (e.g. 1)").addHelpText("after", `
574
+ Examples:
575
+ $ lifi tools
576
+ $ lifi tools --json | jq '.bridges[] | .key'`).action(async (options, command) => {
577
+ const opts = command.optsWithGlobals();
578
+ try {
579
+ const params = {};
580
+ if (options["chain"]) params["chains"] = options["chain"];
581
+ const { data } = await withSpinner("Fetching tools...", () => api.get("/tools", { params }));
582
+ if (isJsonMode(opts)) console.log(jsonOutput(data));
583
+ else {
584
+ console.log("\nBridges:");
585
+ const bridgeRows = data.bridges.map((b) => [b.key, b.name]);
586
+ console.log(formatTable(["Key", "Name"], bridgeRows));
587
+ console.log("\nExchanges:");
588
+ const exchangeRows = data.exchanges.map((e) => [e.key, e.name]);
589
+ console.log(formatTable(["Key", "Name"], exchangeRows));
590
+ console.log("\n Use keys with: lifi quote --allow-bridges <key> --allow-exchanges <key>");
591
+ }
592
+ } catch (error) {
593
+ handleError(error);
594
+ }
595
+ });
596
+ }
597
+ //#endregion
598
+ //#region src/bin/lifi.ts
599
+ function createProgram() {
600
+ const program = new Command();
601
+ program.name("lifi").description("CLI for the LI.FI cross-chain bridge & DEX aggregation API").version("0.1.0").option("--json", "Output raw JSON instead of formatted tables").option("--no-color", "Disable colored output").option("--no-input", "Disable interactive prompts (for scripting)").option("--verbose", "Show verbose output including full API responses").addHelpText("after", "\nDocs: https://docs.li.fi/api-reference/introduction\nRepo: https://github.com/lifinance/lifi-cli");
602
+ program.hook("preAction", (thisCommand) => {
603
+ const opts = thisCommand.opts();
604
+ if (opts["color"] === false) process.env["NO_COLOR"] = "1";
605
+ if (opts["verbose"]) process.env["LIFI_VERBOSE"] = "1";
606
+ if (opts["input"] === false) process.env["LIFI_NO_INPUT"] = "1";
607
+ });
608
+ registerAuthCommand(program);
609
+ registerChainsCommand(program);
610
+ registerTokensCommand(program);
611
+ registerQuoteCommand(program);
612
+ registerRoutesCommand(program);
613
+ registerStatusCommand(program);
614
+ registerConnectionsCommand(program);
615
+ registerToolsCommand(program);
616
+ registerGasCommand(program);
617
+ registerHealthCommand(program);
618
+ return program;
619
+ }
620
+ if (process.env["VITEST"] === void 0) createProgram().parseAsync(process.argv).catch(handleError);
621
+ //#endregion
622
+ export { createProgram };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@lifi/cli",
3
+ "version": "0.1.1-alpha.1",
4
+ "description": "CLI for the LI.FI cross-chain bridge & DEX aggregation API",
5
+ "homepage": "https://github.com/lifinance/lifi-cli",
6
+ "bugs": {
7
+ "url": "https://github.com/lifinance/lifi-cli/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/lifinance/lifi-cli.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "LI.FI <github@li.finance>",
15
+ "keywords": [
16
+ "lifi",
17
+ "lifinance",
18
+ "cli",
19
+ "bridge",
20
+ "dex",
21
+ "cross-chain",
22
+ "aggregator",
23
+ "swap"
24
+ ],
25
+ "type": "module",
26
+ "bin": {
27
+ "lifi": "./dist/lifi.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "dependencies": {
40
+ "@inquirer/prompts": "^8.0.0",
41
+ "axios": "^1.7.0",
42
+ "cli-table3": "^0.6.5",
43
+ "commander": "^12.1.0",
44
+ "ora": "^8.1.0"
45
+ },
46
+ "lint-staged": {
47
+ "src/**/*.{ts,tsx}": [
48
+ "biome check --no-errors-on-unmatched"
49
+ ]
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "^2.4.11",
53
+ "@types/node": "^22.0.0",
54
+ "commit-and-tag-version": "^12.5.0",
55
+ "husky": "^9.1.7",
56
+ "lint-staged": "^16.4.0",
57
+ "tsdown": "^0.21.8",
58
+ "typescript": "~5.6.0",
59
+ "vitest": "^2.1.0"
60
+ },
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "dev": "tsdown --watch",
64
+ "start": "node dist/lifi.js",
65
+ "typecheck": "tsc --noEmit",
66
+ "lint": "biome check src/",
67
+ "lint:fix": "biome check --write src/",
68
+ "format": "biome format --write src/",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "clean": "rm -rf dist",
72
+ "release": "commit-and-tag-version -a -s",
73
+ "release:alpha": "commit-and-tag-version -a -s --prerelease alpha",
74
+ "release:beta": "commit-and-tag-version -a -s --prerelease beta"
75
+ }
76
+ }