@darksol/terminal 0.7.1 → 0.8.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/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2026 DARKSOL <chris00claw@gmail.com>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
package/README.md CHANGED
@@ -12,10 +12,10 @@
12
12
  A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, casino, prepaid cards, builder indexing, secure agent signing, and more. Encrypted wallet management. Agent-native. OpenClaw-controllable.
13
13
 
14
14
  [![npm](https://img.shields.io/npm/v/@darksol/terminal)](https://www.npmjs.com/package/@darksol/terminal)
15
- [![License: MIT](https://img.shields.io/badge/License-MIT-gold.svg)](https://opensource.org/licenses/MIT)
15
+ [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-gold.svg)](https://www.gnu.org/licenses/gpl-3.0)
16
16
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
17
17
 
18
- - Current release: **0.7.1**
18
+ - Current release: **0.8.0**
19
19
  - Changelog: `CHANGELOG.md`
20
20
 
21
21
  ## Install
@@ -48,9 +48,14 @@ darksol watch AERO --above 2.0
48
48
  # Gas estimates
49
49
  darksol gas base
50
50
 
51
- # Swap tokens (Uniswap V3 with slippage protection)
51
+ # Swap tokens (LI.FI — best route across 31 DEXs, Uniswap V3 fallback)
52
52
  darksol trade swap -i ETH -o USDC -a 0.1
53
53
 
54
+ # Cross-chain bridge (60+ chains via LI.FI)
55
+ darksol bridge send --from base --to arbitrum --token ETH -a 0.1
56
+ darksol bridge status 0xTxHash...
57
+ darksol bridge chains
58
+
54
59
  # AI trading assistant
55
60
  darksol ai chat
56
61
 
@@ -71,7 +76,8 @@ darksol agent start main
71
76
 
72
77
  - Arrow-key menus (`↑/↓` + `Enter`) for wallet/config/trade flows
73
78
  - **Interactive send** — token → recipient → amount → password → on-chain transfer
74
- - **Interactive swap** — pair picker (presets + custom) → amount → password → Uniswap V3 execution
79
+ - **Interactive swap** — pair picker (presets + custom) → amount → password → LI.FI execution (Uniswap V3 fallback)
80
+ - **Interactive bridge** — source chain → dest chain → token → amount → password → cross-chain via LI.FI
75
81
  - **Interactive snipe** — contract input → amount → password → fast buy
76
82
  - Wallet picker + wallet action menu (receive/send/portfolio/history/switch chain)
77
83
  - Agent signer control center (`agent`) with guided wallet selection + start/stop/status
@@ -87,7 +93,8 @@ Useful web-shell commands:
87
93
 
88
94
  ```bash
89
95
  help # clickable command menu (arrow keys + Enter)
90
- trade # interactive swap / snipe menu
96
+ trade # interactive swap / snipe / bridge menu
97
+ bridge # cross-chain bridge (LI.FI)
91
98
  send # interactive token transfer
92
99
  wallet # interactive wallet picker and actions
93
100
  keys # provider status + interactive add/update
@@ -104,7 +111,8 @@ ai <prompt> # chat with trading assistant
104
111
  | `wallet` | Create/import/manage encrypted EVM wallets | Free |
105
112
  | `send` | Send ETH or ERC-20 tokens | Gas only |
106
113
  | `receive` | Show receive address + chain safety hints | Free |
107
- | `trade` | Swap (Uniswap V3), snipe (V2), token trading | Gas only |
114
+ | `trade` | Swap via LI.FI (31 DEXs) + Uniswap V3 fallback, snipe | Gas only |
115
+ | `bridge` | Cross-chain bridge via LI.FI (60 chains, 27 bridges) | Gas only |
108
116
  | `dca` | Dollar-cost averaging engine | Gas only |
109
117
  | `ai` | LLM-powered trading assistant & intent execution | Provider dependent |
110
118
  | `agent` | Secure agent signer (PK-isolated proxy) | Free |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "defi"
25
25
  ],
26
26
  "author": "DARKSOL <chris00claw@gmail.com>",
27
- "license": "MIT",
27
+ "license": "GPL-3.0-or-later",
28
28
  "dependencies": {
29
29
  "agentmail": "^0.4.2",
30
30
  "blessed": "^0.1.81",
package/src/cli.js CHANGED
@@ -13,6 +13,7 @@ import { startWebShell } from './web/server.js';
13
13
  import { executeSwap } from './trading/swap.js';
14
14
  import { snipeToken, watchSnipe } from './trading/snipe.js';
15
15
  import { createDCA, listDCA, cancelDCA, runDCA } from './trading/dca.js';
16
+ import { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedChains } from './services/lifi.js';
16
17
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
17
18
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
18
19
  import { casinoBet, casinoTables, casinoStats, casinoReceipt, casinoHealth, casinoVerify } from './services/casino.js';
@@ -113,7 +114,7 @@ export function cli(argv) {
113
114
 
114
115
  trade
115
116
  .command('swap')
116
- .description('Swap tokens via DEX (interactive if flags omitted)')
117
+ .description('Swap tokens via LI.FI (58 chains, 31 DEXs) with Uniswap fallback')
117
118
  .option('-i, --in <token>', 'Token to sell (symbol or address)')
118
119
  .option('-o, --out <token>', 'Token to buy (symbol or address)')
119
120
  .option('-a, --amount <amount>', 'Amount to swap')
@@ -121,6 +122,7 @@ export function cli(argv) {
121
122
  .option('-w, --wallet <name>', 'Wallet to use')
122
123
  .option('-p, --password <pw>', 'Wallet password (non-interactive)')
123
124
  .option('-y, --yes', 'Skip confirmation')
125
+ .option('--direct', 'Force direct Uniswap V3 (skip LI.FI)')
124
126
  .action(async (opts) => {
125
127
  let tokenIn = opts.in;
126
128
  let tokenOut = opts.out;
@@ -138,7 +140,7 @@ export function cli(argv) {
138
140
  amount = answers.amount;
139
141
  }
140
142
 
141
- return executeSwap({
143
+ const swapOpts = {
142
144
  tokenIn,
143
145
  tokenOut,
144
146
  amount,
@@ -146,7 +148,30 @@ export function cli(argv) {
146
148
  wallet: opts.wallet,
147
149
  password: opts.password,
148
150
  confirm: opts.yes ? true : undefined,
149
- });
151
+ };
152
+
153
+ // Try LI.FI first (unless --direct flag)
154
+ if (!opts.direct) {
155
+ try {
156
+ const result = await executeLifiSwap(swapOpts);
157
+ if (result?.success) return;
158
+ // If LI.FI failed (not cancelled), fall back to direct
159
+ if (result?.error !== 'cancelled') {
160
+ const { warn: showWarn, info: showInfo } = await import('./ui/components.js');
161
+ showWarn('LI.FI route failed — falling back to direct Uniswap V3...');
162
+ console.log('');
163
+ } else {
164
+ return; // User cancelled, don't fallback
165
+ }
166
+ } catch {
167
+ const { warn: showWarn } = await import('./ui/components.js');
168
+ showWarn('LI.FI unavailable — falling back to direct Uniswap V3...');
169
+ console.log('');
170
+ }
171
+ }
172
+
173
+ // Direct Uniswap V3 fallback
174
+ return executeSwap(swapOpts);
150
175
  });
151
176
 
152
177
  trade
@@ -193,6 +218,71 @@ export function cli(argv) {
193
218
  console.log('');
194
219
  });
195
220
 
221
+ // ═══════════════════════════════════════
222
+ // BRIDGE COMMANDS (LI.FI)
223
+ // ═══════════════════════════════════════
224
+ const bridge = program
225
+ .command('bridge')
226
+ .description('Cross-chain bridge — move tokens between chains via LI.FI');
227
+
228
+ bridge
229
+ .command('send')
230
+ .description('Bridge tokens to another chain')
231
+ .option('-f, --from <chain>', 'Source chain (e.g. base, ethereum)')
232
+ .option('-t, --to <chain>', 'Destination chain (e.g. arbitrum, optimism)')
233
+ .option('--token <symbol>', 'Token to bridge (e.g. ETH, USDC)', 'ETH')
234
+ .option('-a, --amount <amount>', 'Amount to bridge')
235
+ .option('-s, --slippage <percent>', 'Max slippage %', '0.5')
236
+ .option('-w, --wallet <name>', 'Wallet to use')
237
+ .option('-p, --password <pw>', 'Wallet password (non-interactive)')
238
+ .option('-y, --yes', 'Skip confirmation')
239
+ .action(async (opts) => {
240
+ let fromChain = opts.from;
241
+ let toChain = opts.to;
242
+ let token = opts.token;
243
+ let amount = opts.amount;
244
+
245
+ if (!fromChain || !toChain || !amount) {
246
+ const inquirer = (await import('inquirer')).default;
247
+ const answers = await inquirer.prompt([
248
+ { type: 'input', name: 'fromChain', message: 'Source chain:', default: fromChain || getConfig('chain') || 'base' },
249
+ { type: 'input', name: 'toChain', message: 'Destination chain:', default: toChain || 'arbitrum' },
250
+ { type: 'input', name: 'token', message: 'Token to bridge:', default: token || 'ETH' },
251
+ { type: 'input', name: 'amount', message: 'Amount:', default: amount || '0.1' },
252
+ ]);
253
+ fromChain = answers.fromChain;
254
+ toChain = answers.toChain;
255
+ token = answers.token;
256
+ amount = answers.amount;
257
+ }
258
+
259
+ return executeLifiBridge({
260
+ fromChain,
261
+ toChain,
262
+ token,
263
+ amount,
264
+ slippage: parseFloat(opts.slippage),
265
+ wallet: opts.wallet,
266
+ password: opts.password,
267
+ confirm: opts.yes ? true : undefined,
268
+ });
269
+ });
270
+
271
+ bridge
272
+ .command('status <txHash>')
273
+ .description('Check bridge transfer status')
274
+ .option('-f, --from <chain>', 'Source chain')
275
+ .option('-t, --to <chain>', 'Destination chain')
276
+ .action((txHash, opts) => checkBridgeStatus(txHash, {
277
+ fromChain: opts.from,
278
+ toChain: opts.to,
279
+ }));
280
+
281
+ bridge
282
+ .command('chains')
283
+ .description('Show all supported chains')
284
+ .action(() => showSupportedChains());
285
+
196
286
  // ═══════════════════════════════════════
197
287
  // DCA COMMANDS
198
288
  // ═══════════════════════════════════════
@@ -1109,6 +1199,7 @@ function showCommandList() {
1109
1199
  ['watch', 'Live price monitoring + alerts'],
1110
1200
  ['gas', 'Gas prices & cost estimates'],
1111
1201
  ['trade', 'Swap tokens, snipe, trading'],
1202
+ ['bridge', 'Cross-chain bridge (LI.FI)'],
1112
1203
  ['dca', 'Dollar-cost averaging orders'],
1113
1204
  ['ai chat', 'Standalone AI chat session'],
1114
1205
  ['ai execute', 'Parse + execute a trade via AI'],
@@ -178,6 +178,14 @@ export const SERVICES = {
178
178
  docsUrl: 'https://developers.paraswap.network',
179
179
  validate: (key) => key.length > 5,
180
180
  },
181
+ lifi: {
182
+ name: 'LI.FI',
183
+ category: 'trading',
184
+ description: 'Cross-chain swaps & bridges — 58 chains, 27 bridges, 31 DEXs',
185
+ envVar: 'LIFI_API_KEY',
186
+ docsUrl: 'https://docs.li.fi/api-reference/rate-limits',
187
+ validate: (key) => key.length > 20,
188
+ },
181
189
  };
182
190
 
183
191
  // ──────────────────────────────────────────────────
@@ -0,0 +1,713 @@
1
+ /**
2
+ * LI.FI API Client — Cross-chain swaps & bridges
3
+ * https://docs.li.fi/agents/overview
4
+ *
5
+ * Primary swap/bridge engine for DARKSOL Terminal.
6
+ * Free tier: 200 req/2hr (no key), 200 req/min (with key).
7
+ */
8
+
9
+ import { getKeyAuto, getKeyFromEnv, hasKey } from '../config/keys.js';
10
+ import { getConfig, getRPC } from '../config/store.js';
11
+ import { theme } from '../ui/theme.js';
12
+ import { spinner, kvDisplay, success, error, warn, info, formatAddress } from '../ui/components.js';
13
+ import { showSection } from '../ui/banner.js';
14
+ import { getSigner } from '../wallet/manager.js';
15
+ import { ethers } from 'ethers';
16
+ import inquirer from 'inquirer';
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { homedir } from 'os';
20
+
21
+ const BASE_URL = 'https://li.quest/v1';
22
+ const CACHE_DIR = join(homedir(), '.darksol', 'cache');
23
+ const CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours for chains/tokens
24
+
25
+ // Chain name → LI.FI chain ID mapping
26
+ const CHAIN_IDS = {
27
+ ethereum: 1,
28
+ base: 8453,
29
+ arbitrum: 42161,
30
+ optimism: 10,
31
+ polygon: 137,
32
+ avalanche: 43114,
33
+ bsc: 56,
34
+ gnosis: 100,
35
+ fantom: 250,
36
+ zksync: 324,
37
+ scroll: 534352,
38
+ linea: 59144,
39
+ mantle: 5000,
40
+ celo: 42220,
41
+ blast: 81457,
42
+ mode: 34443,
43
+ };
44
+
45
+ // Reverse: chain ID → name
46
+ const CHAIN_NAMES = Object.fromEntries(
47
+ Object.entries(CHAIN_IDS).map(([name, id]) => [id, name])
48
+ );
49
+
50
+ // ──────────────────────────────────────────────────
51
+ // HTTP HELPER
52
+ // ──────────────────────────────────────────────────
53
+
54
+ function getHeaders() {
55
+ const headers = { 'Accept': 'application/json' };
56
+ // Try vault first, then env
57
+ const apiKey = getKeyAuto('lifi') || getKeyFromEnv('lifi');
58
+ if (apiKey) {
59
+ headers['x-lifi-api-key'] = apiKey;
60
+ }
61
+ return headers;
62
+ }
63
+
64
+ async function lifiGet(endpoint, params = {}) {
65
+ const url = new URL(`${BASE_URL}${endpoint}`);
66
+ for (const [k, v] of Object.entries(params)) {
67
+ if (v !== undefined && v !== null) url.searchParams.set(k, v);
68
+ }
69
+
70
+ const resp = await fetch(url.toString(), { headers: getHeaders() });
71
+
72
+ if (resp.status === 429) {
73
+ const reset = resp.headers.get('ratelimit-reset');
74
+ throw new Error(`Rate limited. Resets in ${reset || '?'}s. Add a free API key: darksol keys add lifi`);
75
+ }
76
+
77
+ if (!resp.ok) {
78
+ const body = await resp.text().catch(() => '');
79
+ throw new Error(`LI.FI API ${resp.status}: ${body}`);
80
+ }
81
+
82
+ return resp.json();
83
+ }
84
+
85
+ // ──────────────────────────────────────────────────
86
+ // CACHE HELPERS
87
+ // ──────────────────────────────────────────────────
88
+
89
+ function ensureCacheDir() {
90
+ if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
91
+ }
92
+
93
+ function getCached(key) {
94
+ try {
95
+ const path = join(CACHE_DIR, `lifi-${key}.json`);
96
+ if (!existsSync(path)) return null;
97
+ const data = JSON.parse(readFileSync(path, 'utf8'));
98
+ if (Date.now() - data.ts > CACHE_TTL) return null;
99
+ return data.value;
100
+ } catch { return null; }
101
+ }
102
+
103
+ function setCache(key, value) {
104
+ try {
105
+ ensureCacheDir();
106
+ writeFileSync(
107
+ join(CACHE_DIR, `lifi-${key}.json`),
108
+ JSON.stringify({ ts: Date.now(), value })
109
+ );
110
+ } catch { /* cache write failures are non-fatal */ }
111
+ }
112
+
113
+ // ──────────────────────────────────────────────────
114
+ // PUBLIC API
115
+ // ──────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Get supported chains (cached)
119
+ */
120
+ export async function getChains() {
121
+ const cached = getCached('chains');
122
+ if (cached) return cached;
123
+ const data = await lifiGet('/chains');
124
+ setCache('chains', data.chains);
125
+ return data.chains;
126
+ }
127
+
128
+ /**
129
+ * Get supported tokens for a chain (cached)
130
+ */
131
+ export async function getTokens(chainId) {
132
+ const cacheKey = `tokens-${chainId}`;
133
+ const cached = getCached(cacheKey);
134
+ if (cached) return cached;
135
+ const data = await lifiGet('/tokens', { chains: chainId });
136
+ const tokens = data.tokens?.[chainId] || [];
137
+ setCache(cacheKey, tokens);
138
+ return tokens;
139
+ }
140
+
141
+ /**
142
+ * Get a quote for a swap or bridge
143
+ * Returns a ready-to-sign transaction
144
+ */
145
+ export async function getQuote({
146
+ fromChain,
147
+ toChain,
148
+ fromToken,
149
+ toToken,
150
+ fromAmount,
151
+ fromAddress,
152
+ slippage,
153
+ }) {
154
+ const fromChainId = typeof fromChain === 'number' ? fromChain : CHAIN_IDS[fromChain];
155
+ const toChainId = typeof toChain === 'number' ? toChain : CHAIN_IDS[toChain];
156
+
157
+ if (!fromChainId) throw new Error(`Unknown chain: ${fromChain}`);
158
+ if (!toChainId) throw new Error(`Unknown chain: ${toChain}`);
159
+
160
+ return lifiGet('/quote', {
161
+ fromChain: fromChainId,
162
+ toChain: toChainId,
163
+ fromToken,
164
+ toToken,
165
+ fromAmount,
166
+ fromAddress,
167
+ slippage: slippage ? slippage / 100 : 0.005, // LI.FI uses decimal (0.005 = 0.5%)
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Check transfer status (for cross-chain)
173
+ */
174
+ export async function getStatus(txHash, opts = {}) {
175
+ return lifiGet('/status', {
176
+ txHash,
177
+ fromChain: opts.fromChain,
178
+ toChain: opts.toChain,
179
+ bridge: opts.bridge,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Get available bridges and exchanges
185
+ */
186
+ export async function getTools() {
187
+ const cached = getCached('tools');
188
+ if (cached) return cached;
189
+ const data = await lifiGet('/tools');
190
+ setCache('tools', data);
191
+ return data;
192
+ }
193
+
194
+ /**
195
+ * Resolve chain name to LI.FI chain ID
196
+ */
197
+ export function resolveChainId(chain) {
198
+ if (typeof chain === 'number') return chain;
199
+ return CHAIN_IDS[chain.toLowerCase()] || null;
200
+ }
201
+
202
+ /**
203
+ * Resolve chain ID to name
204
+ */
205
+ export function resolveChainName(chainId) {
206
+ return CHAIN_NAMES[chainId] || `chain-${chainId}`;
207
+ }
208
+
209
+ /**
210
+ * Check if LI.FI has an API key configured
211
+ */
212
+ export function hasLifiKey() {
213
+ return hasKey('lifi');
214
+ }
215
+
216
+ // ──────────────────────────────────────────────────
217
+ // ERC20 ABI (for approvals)
218
+ // ──────────────────────────────────────────────────
219
+
220
+ const ERC20_ABI = [
221
+ 'function approve(address spender, uint256 amount) returns (bool)',
222
+ 'function allowance(address owner, address spender) view returns (uint256)',
223
+ 'function balanceOf(address) view returns (uint256)',
224
+ 'function decimals() view returns (uint8)',
225
+ 'function symbol() view returns (string)',
226
+ ];
227
+
228
+ // ──────────────────────────────────────────────────
229
+ // SWAP VIA LI.FI
230
+ // ──────────────────────────────────────────────────
231
+
232
+ /**
233
+ * Execute a swap via LI.FI (same-chain)
234
+ */
235
+ export async function executeLifiSwap(opts = {}) {
236
+ const {
237
+ tokenIn,
238
+ tokenOut,
239
+ amount,
240
+ wallet: walletName,
241
+ slippage,
242
+ password: providedPassword,
243
+ confirm: providedConfirm,
244
+ } = opts;
245
+
246
+ const chain = getConfig('chain') || 'base';
247
+ const chainId = resolveChainId(chain);
248
+ const maxSlippage = slippage || getConfig('slippage') || 0.5;
249
+
250
+ if (!chainId) {
251
+ error(`Unknown chain: ${chain}`);
252
+ return { success: false, error: 'unknown_chain' };
253
+ }
254
+
255
+ // Get wallet password
256
+ let password = providedPassword;
257
+ if (!password) {
258
+ const prompted = await inquirer.prompt([{
259
+ type: 'password',
260
+ name: 'password',
261
+ message: theme.gold('Wallet password:'),
262
+ mask: '●',
263
+ }]);
264
+ password = prompted.password;
265
+ }
266
+
267
+ const spin = spinner('Getting LI.FI quote...').start();
268
+
269
+ try {
270
+ const { signer, provider, address } = await getSigner(walletName, password);
271
+
272
+ // Resolve token symbols to addresses if needed
273
+ const fromToken = tokenIn.startsWith('0x') ? tokenIn : tokenIn.toUpperCase();
274
+ const toToken = tokenOut.startsWith('0x') ? tokenOut : tokenOut.toUpperCase();
275
+
276
+ // For native ETH, LI.FI uses the zero address or symbol
277
+ const isNativeIn = ['ETH', 'MATIC', 'POL'].includes(fromToken.toUpperCase());
278
+
279
+ // Get balance check
280
+ if (isNativeIn) {
281
+ const balance = await provider.getBalance(address);
282
+ const amountWei = ethers.parseEther(amount.toString());
283
+ if (balance < amountWei) {
284
+ spin.fail('Insufficient balance');
285
+ error(`Need ${amount} ${fromToken}, have ${ethers.formatEther(balance)}`);
286
+ return { success: false, error: 'insufficient_balance' };
287
+ }
288
+ }
289
+
290
+ // Request quote from LI.FI
291
+ // For token amounts, we need to figure out decimals
292
+ let fromAmount;
293
+ if (isNativeIn) {
294
+ fromAmount = ethers.parseEther(amount.toString()).toString();
295
+ } else {
296
+ // Try to get decimals from the token contract
297
+ try {
298
+ const tokenAddr = tokenIn.startsWith('0x') ? tokenIn : await resolveTokenAddress(tokenIn, chainId);
299
+ if (!tokenAddr) {
300
+ spin.fail('Token not found');
301
+ error(`Could not resolve token: ${tokenIn}. Use contract address instead.`);
302
+ return { success: false, error: 'token_not_found' };
303
+ }
304
+ const contract = new ethers.Contract(tokenAddr, ERC20_ABI, provider);
305
+ const decimals = await contract.decimals();
306
+ fromAmount = ethers.parseUnits(amount.toString(), decimals).toString();
307
+ } catch {
308
+ // Default to 18 decimals
309
+ fromAmount = ethers.parseUnits(amount.toString(), 18).toString();
310
+ }
311
+ }
312
+
313
+ const quote = await getQuote({
314
+ fromChain: chainId,
315
+ toChain: chainId,
316
+ fromToken,
317
+ toToken,
318
+ fromAmount,
319
+ fromAddress: address,
320
+ slippage: maxSlippage,
321
+ });
322
+
323
+ if (!quote?.transactionRequest) {
324
+ spin.fail('No route found');
325
+ error('LI.FI could not find a route for this swap. Try a different pair or amount.');
326
+ return { success: false, error: 'no_route' };
327
+ }
328
+
329
+ spin.succeed('Quote received');
330
+
331
+ // Display swap preview
332
+ const action = quote.action || {};
333
+ const estimate = quote.estimate || {};
334
+ const toolName = quote.toolDetails?.name || quote.tool || 'Unknown DEX';
335
+
336
+ showSection('SWAP PREVIEW (LI.FI)');
337
+ kvDisplay([
338
+ ['From', `${amount} ${action.fromToken?.symbol || tokenIn}`],
339
+ ['To', `~${estimate.toAmountMin ? ethers.formatUnits(estimate.toAmountMin, estimate.toToken?.decimals || 18) : '?'} ${action.toToken?.symbol || tokenOut}`],
340
+ ['Route', toolName],
341
+ ['Chain', chain],
342
+ ['Slippage', `${maxSlippage}%`],
343
+ ['Gas Est.', estimate.gasCosts?.[0]?.amountUSD ? `$${estimate.gasCosts[0].amountUSD}` : 'N/A'],
344
+ ]);
345
+ console.log('');
346
+
347
+ // Confirm
348
+ let confirm = providedConfirm;
349
+ if (typeof confirm !== 'boolean') {
350
+ const prompted = await inquirer.prompt([{
351
+ type: 'confirm',
352
+ name: 'confirm',
353
+ message: theme.gold('Execute swap?'),
354
+ default: false,
355
+ }]);
356
+ confirm = prompted.confirm;
357
+ }
358
+
359
+ if (!confirm) {
360
+ warn('Swap cancelled');
361
+ return { success: false, error: 'cancelled' };
362
+ }
363
+
364
+ const swapSpin = spinner('Executing swap...').start();
365
+
366
+ // Handle approval if needed
367
+ const txReq = quote.transactionRequest;
368
+ if (!isNativeIn && txReq.to) {
369
+ const tokenAddr = action.fromToken?.address;
370
+ if (tokenAddr && tokenAddr !== ethers.ZeroAddress) {
371
+ const token = new ethers.Contract(tokenAddr, ERC20_ABI, signer);
372
+ const allowance = await token.allowance(address, txReq.to);
373
+ const needed = BigInt(fromAmount);
374
+ if (allowance < needed) {
375
+ swapSpin.text = 'Approving token...';
376
+ const approveTx = await token.approve(txReq.to, ethers.MaxUint256);
377
+ await approveTx.wait();
378
+ }
379
+ }
380
+ }
381
+
382
+ // Send the transaction
383
+ swapSpin.text = 'Sending transaction...';
384
+ const tx = await signer.sendTransaction({
385
+ to: txReq.to,
386
+ data: txReq.data,
387
+ value: txReq.value ? BigInt(txReq.value) : 0n,
388
+ gasLimit: txReq.gasLimit ? BigInt(txReq.gasLimit) : undefined,
389
+ gasPrice: txReq.gasPrice ? BigInt(txReq.gasPrice) : undefined,
390
+ });
391
+
392
+ swapSpin.text = 'Waiting for confirmation...';
393
+ const receipt = await tx.wait();
394
+
395
+ swapSpin.succeed(theme.success('Swap executed via LI.FI'));
396
+
397
+ console.log('');
398
+ showSection('SWAP RESULT');
399
+ kvDisplay([
400
+ ['TX Hash', receipt.hash],
401
+ ['Block', receipt.blockNumber.toString()],
402
+ ['Gas Used', receipt.gasUsed.toString()],
403
+ ['Route', toolName],
404
+ ['Status', receipt.status === 1 ? theme.success('Success') : theme.error('Failed')],
405
+ ]);
406
+ console.log('');
407
+
408
+ // Nudge for API key if they don't have one
409
+ showKeyNudge();
410
+
411
+ return { success: true, hash: receipt.hash };
412
+
413
+ } catch (err) {
414
+ spin.fail('Swap failed');
415
+ error(err.message);
416
+ return { success: false, error: err.message };
417
+ }
418
+ }
419
+
420
+ // ──────────────────────────────────────────────────
421
+ // BRIDGE VIA LI.FI
422
+ // ──────────────────────────────────────────────────
423
+
424
+ /**
425
+ * Bridge tokens cross-chain via LI.FI
426
+ */
427
+ export async function executeLifiBridge(opts = {}) {
428
+ const {
429
+ fromChain: fromChainName,
430
+ toChain: toChainName,
431
+ token: tokenSymbol,
432
+ amount,
433
+ wallet: walletName,
434
+ slippage,
435
+ password: providedPassword,
436
+ confirm: providedConfirm,
437
+ } = opts;
438
+
439
+ const maxSlippage = slippage || getConfig('slippage') || 0.5;
440
+ const fromChainId = resolveChainId(fromChainName);
441
+ const toChainId = resolveChainId(toChainName);
442
+
443
+ if (!fromChainId) { error(`Unknown source chain: ${fromChainName}`); return; }
444
+ if (!toChainId) { error(`Unknown destination chain: ${toChainName}`); return; }
445
+ if (fromChainId === toChainId) { error('Source and destination chains must be different. Use `trade swap` for same-chain.'); return; }
446
+
447
+ // Get wallet password
448
+ let password = providedPassword;
449
+ if (!password) {
450
+ const prompted = await inquirer.prompt([{
451
+ type: 'password',
452
+ name: 'password',
453
+ message: theme.gold('Wallet password:'),
454
+ mask: '●',
455
+ }]);
456
+ password = prompted.password;
457
+ }
458
+
459
+ const spin = spinner('Getting bridge quote from LI.FI...').start();
460
+
461
+ try {
462
+ // Connect to source chain
463
+ const rpcUrl = getRPC(fromChainName);
464
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
465
+ const { signer, address } = await getSigner(walletName, password);
466
+ const connectedSigner = signer.connect(provider);
467
+
468
+ const fromToken = tokenSymbol.toUpperCase();
469
+ const isNativeIn = ['ETH', 'MATIC', 'POL'].includes(fromToken);
470
+
471
+ // Calculate amount in wei
472
+ let fromAmount;
473
+ if (isNativeIn) {
474
+ fromAmount = ethers.parseEther(amount.toString()).toString();
475
+ } else {
476
+ // Default to 6 decimals for stablecoins, 18 for others
477
+ const isStable = ['USDC', 'USDT', 'DAI', 'USDB'].includes(fromToken);
478
+ fromAmount = ethers.parseUnits(amount.toString(), isStable ? 6 : 18).toString();
479
+ }
480
+
481
+ const quote = await getQuote({
482
+ fromChain: fromChainId,
483
+ toChain: toChainId,
484
+ fromToken,
485
+ toToken: fromToken, // Same token on dest chain by default
486
+ fromAmount,
487
+ fromAddress: address,
488
+ slippage: maxSlippage,
489
+ });
490
+
491
+ if (!quote?.transactionRequest) {
492
+ spin.fail('No bridge route found');
493
+ error('LI.FI could not find a bridge route. Try a different token or amount.');
494
+ return;
495
+ }
496
+
497
+ spin.succeed('Bridge quote received');
498
+
499
+ const action = quote.action || {};
500
+ const estimate = quote.estimate || {};
501
+ const toolName = quote.toolDetails?.name || quote.tool || 'Unknown Bridge';
502
+ const estTime = estimate.executionDuration ? `~${Math.ceil(estimate.executionDuration / 60)} min` : 'varies';
503
+
504
+ showSection('BRIDGE PREVIEW (LI.FI)');
505
+ kvDisplay([
506
+ ['From', `${amount} ${fromToken} on ${fromChainName}`],
507
+ ['To', `~${estimate.toAmountMin ? ethers.formatUnits(estimate.toAmountMin, estimate.toToken?.decimals || 18) : '?'} ${action.toToken?.symbol || fromToken} on ${toChainName}`],
508
+ ['Bridge', toolName],
509
+ ['Est. Time', estTime],
510
+ ['Slippage', `${maxSlippage}%`],
511
+ ['Gas Est.', estimate.gasCosts?.[0]?.amountUSD ? `$${estimate.gasCosts[0].amountUSD}` : 'N/A'],
512
+ ]);
513
+ console.log('');
514
+
515
+ // Confirm
516
+ let confirm = providedConfirm;
517
+ if (typeof confirm !== 'boolean') {
518
+ const prompted = await inquirer.prompt([{
519
+ type: 'confirm',
520
+ name: 'confirm',
521
+ message: theme.gold('Execute bridge?'),
522
+ default: false,
523
+ }]);
524
+ confirm = prompted.confirm;
525
+ }
526
+
527
+ if (!confirm) { warn('Bridge cancelled'); return; }
528
+
529
+ const bridgeSpin = spinner('Executing bridge...').start();
530
+
531
+ // Handle approval
532
+ const txReq = quote.transactionRequest;
533
+ if (!isNativeIn && txReq.to) {
534
+ const tokenAddr = action.fromToken?.address;
535
+ if (tokenAddr && tokenAddr !== ethers.ZeroAddress) {
536
+ const token = new ethers.Contract(tokenAddr, ERC20_ABI, connectedSigner);
537
+ const allowance = await token.allowance(address, txReq.to);
538
+ if (allowance < BigInt(fromAmount)) {
539
+ bridgeSpin.text = 'Approving token...';
540
+ const approveTx = await token.approve(txReq.to, ethers.MaxUint256);
541
+ await approveTx.wait();
542
+ }
543
+ }
544
+ }
545
+
546
+ // Send bridge transaction
547
+ bridgeSpin.text = 'Sending bridge transaction...';
548
+ const tx = await connectedSigner.sendTransaction({
549
+ to: txReq.to,
550
+ data: txReq.data,
551
+ value: txReq.value ? BigInt(txReq.value) : 0n,
552
+ gasLimit: txReq.gasLimit ? BigInt(txReq.gasLimit) : undefined,
553
+ });
554
+
555
+ bridgeSpin.text = 'Waiting for source chain confirmation...';
556
+ const receipt = await tx.wait();
557
+
558
+ bridgeSpin.succeed(theme.success('Bridge transaction submitted'));
559
+
560
+ console.log('');
561
+ showSection('BRIDGE RESULT');
562
+ kvDisplay([
563
+ ['TX Hash', receipt.hash],
564
+ ['Source Chain', fromChainName],
565
+ ['Dest Chain', toChainName],
566
+ ['Bridge', toolName],
567
+ ['Est. Arrival', estTime],
568
+ ['Status', receipt.status === 1 ? theme.success('Submitted') : theme.error('Failed')],
569
+ ]);
570
+ console.log('');
571
+ info(`Track status: darksol bridge status ${receipt.hash} --from ${fromChainName} --to ${toChainName}`);
572
+ console.log('');
573
+
574
+ showKeyNudge();
575
+
576
+ } catch (err) {
577
+ spin.fail('Bridge failed');
578
+ error(err.message);
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Check bridge transfer status
584
+ */
585
+ export async function checkBridgeStatus(txHash, opts = {}) {
586
+ const spin = spinner('Checking bridge status...').start();
587
+
588
+ try {
589
+ const status = await getStatus(txHash, {
590
+ fromChain: opts.fromChain ? resolveChainId(opts.fromChain) : undefined,
591
+ toChain: opts.toChain ? resolveChainId(opts.toChain) : undefined,
592
+ });
593
+
594
+ spin.succeed('Status retrieved');
595
+
596
+ showSection('BRIDGE STATUS');
597
+
598
+ const sending = status.sending || {};
599
+ const receiving = status.receiving || {};
600
+
601
+ kvDisplay([
602
+ ['Status', formatBridgeStatus(status.status)],
603
+ ['Substatus', status.substatus || 'N/A'],
604
+ ['Source TX', sending.txHash || txHash],
605
+ ['Source Chain', sending.chainId ? resolveChainName(sending.chainId) : 'N/A'],
606
+ ['Dest TX', receiving.txHash || theme.dim('pending...')],
607
+ ['Dest Chain', receiving.chainId ? resolveChainName(receiving.chainId) : 'N/A'],
608
+ ['Bridge', status.tool || 'N/A'],
609
+ ]);
610
+ console.log('');
611
+
612
+ if (status.status === 'PENDING') {
613
+ info('Bridge is still in progress. Check again in a few minutes.');
614
+ }
615
+
616
+ } catch (err) {
617
+ spin.fail('Status check failed');
618
+ error(err.message);
619
+ }
620
+ }
621
+
622
+ function formatBridgeStatus(status) {
623
+ switch (status) {
624
+ case 'DONE': return theme.success('✓ Complete');
625
+ case 'PENDING': return theme.gold('⏳ Pending');
626
+ case 'FAILED': return theme.error('✗ Failed');
627
+ case 'NOT_FOUND': return theme.dim('Not found');
628
+ case 'PARTIAL': return theme.accent('⚠ Partial');
629
+ default: return status || 'Unknown';
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Show supported chains
635
+ */
636
+ export async function showSupportedChains() {
637
+ const spin = spinner('Fetching supported chains...').start();
638
+
639
+ try {
640
+ const chains = await getChains();
641
+ spin.succeed(`${chains.length} chains supported`);
642
+
643
+ showSection('LI.FI SUPPORTED CHAINS');
644
+
645
+ // Group by type
646
+ const evm = chains.filter(c => c.chainType === 'EVM');
647
+ const svm = chains.filter(c => c.chainType === 'SVM');
648
+ const other = chains.filter(c => !['EVM', 'SVM'].includes(c.chainType));
649
+
650
+ console.log(theme.gold(' EVM Chains:'));
651
+ for (const c of evm.sort((a, b) => a.name.localeCompare(b.name))) {
652
+ const configured = CHAIN_IDS[c.key] ? theme.success('●') : theme.dim('○');
653
+ console.log(` ${configured} ${theme.label(c.name.padEnd(20))} ${theme.dim(`id:${c.id}`)}`);
654
+ }
655
+
656
+ if (svm.length) {
657
+ console.log('');
658
+ console.log(theme.gold(' Solana:'));
659
+ for (const c of svm) {
660
+ console.log(` ${theme.dim('○')} ${theme.label(c.name.padEnd(20))} ${theme.dim(`id:${c.id}`)}`);
661
+ }
662
+ }
663
+
664
+ if (other.length) {
665
+ console.log('');
666
+ console.log(theme.gold(' Other:'));
667
+ for (const c of other) {
668
+ console.log(` ${theme.dim('○')} ${theme.label(c.name.padEnd(20))} ${theme.dim(`type:${c.chainType}`)}`);
669
+ }
670
+ }
671
+
672
+ console.log('');
673
+ info(`Your configured chains: ${Object.keys(CHAIN_IDS).join(', ')}`);
674
+ console.log('');
675
+
676
+ } catch (err) {
677
+ spin.fail('Failed to fetch chains');
678
+ error(err.message);
679
+ }
680
+ }
681
+
682
+ // ──────────────────────────────────────────────────
683
+ // HELPERS
684
+ // ──────────────────────────────────────────────────
685
+
686
+ /**
687
+ * Resolve a token symbol to address using LI.FI token list
688
+ */
689
+ async function resolveTokenAddress(symbol, chainId) {
690
+ try {
691
+ const tokens = await getTokens(chainId);
692
+ const match = tokens.find(t => t.symbol.toUpperCase() === symbol.toUpperCase());
693
+ return match?.address || null;
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+
699
+ // Track nudge state (show max once per session)
700
+ let nudgeShown = false;
701
+
702
+ /**
703
+ * Show a one-time nudge to add a LI.FI API key
704
+ */
705
+ function showKeyNudge() {
706
+ if (nudgeShown || hasLifiKey()) return;
707
+ nudgeShown = true;
708
+ console.log(theme.dim(' 💡 Want cross-chain bridges & faster routing? Add a free LI.FI API key:'));
709
+ console.log(theme.dim(' https://docs.li.fi/api-reference/rate-limits → ') + theme.label('darksol keys add lifi'));
710
+ console.log('');
711
+ }
712
+
713
+ export { CHAIN_IDS, CHAIN_NAMES };
@@ -1,3 +1,4 @@
1
1
  export { executeSwap, resolveToken, getTokenInfo } from './swap.js';
2
2
  export { snipeToken, watchSnipe } from './snipe.js';
3
3
  export { createDCA, listDCA, cancelDCA, runDCA } from './dca.js';
4
+ export { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedChains } from '../services/lifi.js';
@@ -223,7 +223,7 @@ export async function handleMenuSelect(id, value, item, ws) {
223
223
 
224
224
  case 'trade_action':
225
225
  if (value === 'swap') {
226
- ws.sendMenu('trade_swap_pair', '◆ Swap Pair', [
226
+ ws.sendMenu('trade_swap_pair', '◆ Swap Pair (via LI.FI — best route across 31 DEXs)', [
227
227
  { value: 'ETH->USDC', label: 'ETH → USDC', desc: 'Most common' },
228
228
  { value: 'USDC->ETH', label: 'USDC → ETH', desc: 'Reverse' },
229
229
  { value: 'ETH->AERO', label: 'ETH → AERO', desc: 'Base ecosystem' },
@@ -233,6 +233,16 @@ export async function handleMenuSelect(id, value, item, ws) {
233
233
  ]);
234
234
  return {};
235
235
  }
236
+ if (value === 'bridge') {
237
+ const currentChain = getConfig('chain') || 'base';
238
+ const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon', 'avalanche', 'bsc', 'zksync', 'scroll', 'linea'];
239
+ ws.sendMenu('bridge_from_chain', '◆ Bridge From Chain', chains.map(c => ({
240
+ value: c,
241
+ label: c === currentChain ? `★ ${c}` : c,
242
+ desc: c === currentChain ? 'current' : '',
243
+ })));
244
+ return {};
245
+ }
236
246
  if (value === 'snipe') {
237
247
  ws.sendPrompt('trade_snipe_token', 'Token contract (0x...):', {});
238
248
  return {};
@@ -280,6 +290,63 @@ export async function handleMenuSelect(id, value, item, ws) {
280
290
  });
281
291
  return {};
282
292
 
293
+ case 'bridge_from_chain': {
294
+ if (value === 'back') return {};
295
+ const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon', 'avalanche', 'bsc', 'zksync', 'scroll', 'linea'];
296
+ const destChains = chains.filter(c => c !== value);
297
+ ws.sendMenu('bridge_to_chain', `◆ Bridge To Chain (from ${value})`, destChains.map(c => ({
298
+ value: c,
299
+ label: c,
300
+ desc: '',
301
+ meta: { fromChain: value },
302
+ })));
303
+ return {};
304
+ }
305
+
306
+ case 'bridge_to_chain': {
307
+ if (value === 'back') return {};
308
+ const fromChain = item?.meta?.fromChain || 'base';
309
+ ws.sendMenu('bridge_token', `◆ Token to Bridge (${fromChain} → ${value})`, [
310
+ { value: 'ETH', label: 'ETH', desc: 'Native token', meta: { fromChain, toChain: value } },
311
+ { value: 'USDC', label: 'USDC', desc: 'Stablecoin', meta: { fromChain, toChain: value } },
312
+ { value: 'USDT', label: 'USDT', desc: 'Tether', meta: { fromChain, toChain: value } },
313
+ { value: 'custom', label: 'Custom token', desc: 'Enter symbol', meta: { fromChain, toChain: value } },
314
+ { value: 'back', label: '← Back', desc: '' },
315
+ ]);
316
+ return {};
317
+ }
318
+
319
+ case 'bridge_token': {
320
+ if (value === 'back') return {};
321
+ if (value === 'custom') {
322
+ ws.sendPrompt('bridge_custom_token', 'Token symbol or address:', item?.meta || {});
323
+ return {};
324
+ }
325
+ const m = item?.meta || {};
326
+ ws.sendMenu('bridge_amount', `◆ Amount (${value} · ${m.fromChain} → ${m.toChain})`, [
327
+ { value: '0.01', label: `0.01 ${value}`, desc: 'small', meta: { ...m, token: value } },
328
+ { value: '0.05', label: `0.05 ${value}`, desc: 'standard', meta: { ...m, token: value } },
329
+ { value: '0.1', label: `0.1 ${value}`, desc: 'medium', meta: { ...m, token: value } },
330
+ { value: '0.5', label: `0.5 ${value}`, desc: 'large', meta: { ...m, token: value } },
331
+ { value: '1', label: `1 ${value}`, desc: 'large', meta: { ...m, token: value } },
332
+ { value: 'custom', label: 'Custom amount', desc: '', meta: { ...m, token: value } },
333
+ ]);
334
+ return {};
335
+ }
336
+
337
+ case 'bridge_amount': {
338
+ if (value === 'custom') {
339
+ ws.sendPrompt('bridge_custom_amount', `Amount (${item?.meta?.token || 'token'}):`, item?.meta || {});
340
+ return {};
341
+ }
342
+ ws.sendPrompt('bridge_password', `Wallet password (bridge ${value} ${item?.meta?.token || ''} · ${item?.meta?.fromChain || ''} → ${item?.meta?.toChain || ''}):`, {
343
+ ...(item?.meta || {}),
344
+ amount: value,
345
+ mask: true,
346
+ });
347
+ return {};
348
+ }
349
+
283
350
  case 'send_token':
284
351
  if (value === 'back') return {};
285
352
  if (value === 'custom') {
@@ -536,6 +603,66 @@ export async function handlePromptResponse(id, value, meta, ws) {
536
603
  return {};
537
604
  }
538
605
 
606
+ if (id === 'bridge_custom_token') {
607
+ if (!value) { ws.sendLine(` ${ANSI.red}✗ Cancelled${ANSI.reset}`); ws.sendLine(''); return {}; }
608
+ const m = meta || {};
609
+ ws.sendMenu('bridge_amount', `◆ Amount (${value} · ${m.fromChain} → ${m.toChain})`, [
610
+ { value: '0.01', label: `0.01 ${value}`, desc: 'small', meta: { ...m, token: value.trim() } },
611
+ { value: '0.05', label: `0.05 ${value}`, desc: 'standard', meta: { ...m, token: value.trim() } },
612
+ { value: '0.1', label: `0.1 ${value}`, desc: 'medium', meta: { ...m, token: value.trim() } },
613
+ { value: '0.5', label: `0.5 ${value}`, desc: 'large', meta: { ...m, token: value.trim() } },
614
+ { value: '1', label: `1 ${value}`, desc: 'large', meta: { ...m, token: value.trim() } },
615
+ { value: 'custom', label: 'Custom amount', desc: '', meta: { ...m, token: value.trim() } },
616
+ ]);
617
+ return {};
618
+ }
619
+
620
+ if (id === 'bridge_custom_amount') {
621
+ const n = Number(value);
622
+ if (!Number.isFinite(n) || n <= 0) {
623
+ ws.sendLine(` ${ANSI.red}✗ Invalid amount${ANSI.reset}`);
624
+ ws.sendLine('');
625
+ return {};
626
+ }
627
+ ws.sendPrompt('bridge_password', `Wallet password (bridge ${value} ${meta?.token || ''} · ${meta?.fromChain || ''} → ${meta?.toChain || ''}):`, {
628
+ ...meta,
629
+ amount: String(value),
630
+ mask: true,
631
+ });
632
+ return {};
633
+ }
634
+
635
+ if (id === 'bridge_password') {
636
+ if (!meta?.fromChain || !meta?.toChain || !meta?.token || !meta?.amount || !value) {
637
+ ws.sendLine(` ${ANSI.red}✗ Missing bridge details${ANSI.reset}`);
638
+ ws.sendLine('');
639
+ return {};
640
+ }
641
+
642
+ ws.sendLine('');
643
+ ws.sendLine(` ${ANSI.dim}Bridging ${meta.amount} ${meta.token} from ${meta.fromChain} → ${meta.toChain} via LI.FI...${ANSI.reset}`);
644
+ ws.sendLine('');
645
+
646
+ try {
647
+ const { executeLifiBridge } = await import('../services/lifi.js');
648
+ await executeLifiBridge({
649
+ fromChain: meta.fromChain,
650
+ toChain: meta.toChain,
651
+ token: meta.token,
652
+ amount: meta.amount,
653
+ slippage: parseFloat(getConfig('slippage') || 0.5),
654
+ wallet: getConfig('activeWallet'),
655
+ password: value,
656
+ confirm: true,
657
+ });
658
+ ws.sendLine(` ${ANSI.green}✓ Bridge flow completed (check terminal output for receipt)${ANSI.reset}`);
659
+ } catch (err) {
660
+ ws.sendLine(` ${ANSI.red}✗ Bridge failed: ${err.message}${ANSI.reset}`);
661
+ }
662
+ ws.sendLine('');
663
+ return {};
664
+ }
665
+
539
666
  if (id === 'trade_swap_password') {
540
667
  if (!meta?.tokenIn || !meta?.tokenOut || !meta?.amount || !value) {
541
668
  ws.sendLine(` ${ANSI.red}✗ Missing swap details${ANSI.reset}`);
@@ -543,12 +670,13 @@ export async function handlePromptResponse(id, value, meta, ws) {
543
670
  return {};
544
671
  }
545
672
 
546
- ws.sendLine(` ${ANSI.dim}Executing swap ${meta.amount} ${meta.tokenIn} → ${meta.tokenOut}...${ANSI.reset}`);
673
+ ws.sendLine(` ${ANSI.dim}Executing swap ${meta.amount} ${meta.tokenIn} → ${meta.tokenOut} via LI.FI...${ANSI.reset}`);
547
674
  ws.sendLine('');
548
675
 
549
676
  try {
550
- const { executeSwap } = await import('../trading/swap.js');
551
- await executeSwap({
677
+ // Try LI.FI first, fall back to direct Uniswap
678
+ const { executeLifiSwap } = await import('../services/lifi.js');
679
+ const swapOpts = {
552
680
  tokenIn: meta.tokenIn,
553
681
  tokenOut: meta.tokenOut,
554
682
  amount: meta.amount,
@@ -556,8 +684,18 @@ export async function handlePromptResponse(id, value, meta, ws) {
556
684
  wallet: getConfig('activeWallet'),
557
685
  password: value,
558
686
  confirm: true,
559
- });
560
- ws.sendLine(` ${ANSI.green}✓ Swap flow completed (check terminal output for receipt)${ANSI.reset}`);
687
+ };
688
+
689
+ const result = await executeLifiSwap(swapOpts);
690
+ if (result?.success) {
691
+ ws.sendLine(` ${ANSI.green}✓ Swap completed via LI.FI${ANSI.reset}`);
692
+ } else if (result?.error !== 'cancelled') {
693
+ // Fallback to direct Uniswap
694
+ ws.sendLine(` ${ANSI.darkGold}LI.FI route unavailable — trying direct Uniswap V3...${ANSI.reset}`);
695
+ const { executeSwap } = await import('../trading/swap.js');
696
+ await executeSwap(swapOpts);
697
+ ws.sendLine(` ${ANSI.green}✓ Swap completed via Uniswap V3${ANSI.reset}`);
698
+ }
561
699
  } catch (err) {
562
700
  ws.sendLine(` ${ANSI.red}✗ Swap failed: ${err.message}${ANSI.reset}`);
563
701
  }
@@ -686,6 +824,8 @@ export async function handleCommand(cmd, ws) {
686
824
  return await cmdMarket(args, ws);
687
825
  case 'trade':
688
826
  return await cmdTrade(args, ws);
827
+ case 'bridge':
828
+ return await cmdBridge(args, ws);
689
829
  case 'wallet':
690
830
  return await cmdWallet(args, ws);
691
831
  case 'mail':
@@ -719,7 +859,7 @@ export async function handleCommand(cmd, ws) {
719
859
  return await cmdChatLogs(args, ws);
720
860
  default: {
721
861
  // Fuzzy: if it looks like natural language, route to AI
722
- const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca|order|card|prepaid|visa|mastercard|bet|coinflip|flip|dice|slots|hilo|gamble|play|casino)\b/i;
862
+ const nlKeywords = /\b(swap|buy|sell|send|transfer|price|what|how|should|analyze|check|balance|gas|dca|order|card|prepaid|visa|mastercard|bet|coinflip|flip|dice|slots|hilo|gamble|play|casino|bridge|cross-chain|crosschain)\b/i;
723
863
  if (nlKeywords.test(cmd)) {
724
864
  return await cmdAI(cmd.split(/\s+/), ws);
725
865
  }
@@ -986,7 +1126,8 @@ async function cmdTrade(args, ws) {
986
1126
  ws.sendLine('');
987
1127
 
988
1128
  ws.sendMenu('trade_action', '◆ Trade Actions', [
989
- { value: 'swap', label: '🔄 Swap', desc: 'Interactive token swap (password prompt)' },
1129
+ { value: 'swap', label: '🔄 Swap', desc: 'Token swap via LI.FI (best route across 31 DEXs)' },
1130
+ { value: 'bridge', label: '🌉 Bridge', desc: 'Cross-chain transfer via LI.FI (60 chains)' },
990
1131
  { value: 'snipe', label: '⚡ Snipe', desc: 'Fast buy by token contract' },
991
1132
  { value: 'watch', label: '👀 Watch Pairs', desc: 'Monitor new pairs (CLI guidance)' },
992
1133
  { value: 'back', label: '← Back', desc: '' },
@@ -995,6 +1136,79 @@ async function cmdTrade(args, ws) {
995
1136
  return {};
996
1137
  }
997
1138
 
1139
+ // ══════════════════════════════════════════════════
1140
+ // BRIDGE (LI.FI cross-chain)
1141
+ // ══════════════════════════════════════════════════
1142
+ async function cmdBridge(args, ws) {
1143
+ const sub = (args[0] || '').toLowerCase();
1144
+
1145
+ if (sub === 'status' && args[1]) {
1146
+ // Check bridge transfer status
1147
+ ws.sendLine(` ${ANSI.dim}Checking bridge status...${ANSI.reset}`);
1148
+ try {
1149
+ const { checkBridgeStatus } = await import('../services/lifi.js');
1150
+ await checkBridgeStatus(args[1], {
1151
+ fromChain: args.find((a, i) => args[i - 1] === '--from'),
1152
+ toChain: args.find((a, i) => args[i - 1] === '--to'),
1153
+ });
1154
+ } catch (err) {
1155
+ ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
1156
+ }
1157
+ ws.sendLine('');
1158
+ return {};
1159
+ }
1160
+
1161
+ if (sub === 'chains') {
1162
+ ws.sendLine(` ${ANSI.dim}Fetching supported chains...${ANSI.reset}`);
1163
+ try {
1164
+ const { getChains } = await import('../services/lifi.js');
1165
+ const chains = await getChains();
1166
+ ws.sendLine('');
1167
+ ws.sendLine(`${ANSI.gold} ◆ LI.FI SUPPORTED CHAINS (${chains.length})${ANSI.reset}`);
1168
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1169
+
1170
+ const evm = chains.filter(c => c.chainType === 'EVM').sort((a, b) => a.name.localeCompare(b.name));
1171
+ const other = chains.filter(c => c.chainType !== 'EVM');
1172
+
1173
+ for (const c of evm.slice(0, 30)) {
1174
+ ws.sendLine(` ${ANSI.green}●${ANSI.reset} ${ANSI.white}${c.name.padEnd(22)}${ANSI.reset} ${ANSI.dim}id:${c.id}${ANSI.reset}`);
1175
+ }
1176
+ if (evm.length > 30) {
1177
+ ws.sendLine(` ${ANSI.dim}...and ${evm.length - 30} more EVM chains${ANSI.reset}`);
1178
+ }
1179
+ if (other.length) {
1180
+ ws.sendLine('');
1181
+ for (const c of other) {
1182
+ ws.sendLine(` ${ANSI.blue}●${ANSI.reset} ${ANSI.white}${c.name.padEnd(22)}${ANSI.reset} ${ANSI.dim}${c.chainType}${ANSI.reset}`);
1183
+ }
1184
+ }
1185
+ ws.sendLine('');
1186
+ } catch (err) {
1187
+ ws.sendLine(` ${ANSI.red}✗ ${err.message}${ANSI.reset}`);
1188
+ ws.sendLine('');
1189
+ }
1190
+ return {};
1191
+ }
1192
+
1193
+ // Default: show bridge menu
1194
+ const currentChain = getConfig('chain') || 'base';
1195
+
1196
+ ws.sendLine(`${ANSI.gold} ◆ CROSS-CHAIN BRIDGE (LI.FI)${ANSI.reset}`);
1197
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
1198
+ ws.sendLine(` ${ANSI.white}Move tokens between 60+ chains with optimal routing.${ANSI.reset}`);
1199
+ ws.sendLine(` ${ANSI.dim}Aggregates 27 bridges for best rates.${ANSI.reset}`);
1200
+ ws.sendLine('');
1201
+
1202
+ const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon', 'avalanche', 'bsc', 'zksync', 'scroll', 'linea'];
1203
+ ws.sendMenu('bridge_from_chain', '◆ Bridge From Chain', chains.map(c => ({
1204
+ value: c,
1205
+ label: c === currentChain ? `★ ${c}` : c,
1206
+ desc: c === currentChain ? 'current chain' : '',
1207
+ })));
1208
+
1209
+ return {};
1210
+ }
1211
+
998
1212
  // ══════════════════════════════════════════════════
999
1213
  // WALLET
1000
1214
  // ══════════════════════════════════════════════════