@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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/lifi.js +622 -0
- 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
|
+
}
|