@darksol/terminal 0.7.2 → 0.8.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/README.md +13 -5
- package/package.json +1 -1
- package/skill/SKILL.md +19 -6
- package/src/cli.js +94 -3
- package/src/config/keys.js +8 -0
- package/src/services/casino.js +26 -76
- package/src/services/lifi.js +713 -0
- package/src/trading/index.js +1 -0
- package/src/web/commands.js +222 -8
|
@@ -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 };
|