minara 0.4.6 → 0.4.7

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.
@@ -1,4 +1,4 @@
1
- import type { CrossChainSwapDto, CrossChainTransferDto, CrossChainActivitiesDto, CrossChainAccount, WalletAsset, TransactionResult } from '../types.js';
1
+ import type { CrossChainSwapDto, CrossChainTransferDto, CrossChainActivitiesDto, CrossChainAccount, WalletAsset, TransactionResult, CrossChainSwapsSimulateItem } from '../types.js';
2
2
  /** Get cross-chain account info */
3
3
  export declare function getAccount(token: string): Promise<import("../types.js").ApiResponse<CrossChainAccount>>;
4
4
  /** Get wallet assets */
@@ -8,7 +8,7 @@ export declare function swap(token: string, dto: CrossChainSwapDto): Promise<imp
8
8
  /** Execute multiple swaps */
9
9
  export declare function swaps(token: string, swapList: CrossChainSwapDto[]): Promise<import("../types.js").ApiResponse<TransactionResult[]>>;
10
10
  /** Simulate swaps (dry-run) */
11
- export declare function swapsSimulate(token: string, swapList: CrossChainSwapDto[]): Promise<import("../types.js").ApiResponse<TransactionResult[]>>;
11
+ export declare function swapsSimulate(token: string, swapList: CrossChainSwapDto[]): Promise<import("../types.js").ApiResponse<CrossChainSwapsSimulateItem[]>>;
12
12
  /** Transfer tokens */
13
13
  export declare function transfer(token: string, dto: CrossChainTransferDto): Promise<import("../types.js").ApiResponse<TransactionResult>>;
14
14
  /** Get activities */
@@ -20,11 +20,10 @@ function flattenStock(item) {
20
20
  // ─── trending ────────────────────────────────────────────────────────────
21
21
  const trendingCmd = new Command('trending')
22
22
  .description('View trending tokens or stocks')
23
- .argument('[category]', 'tokens or stocks (default: interactive)')
24
23
  .option('-t, --type <category>', 'Trending type: tokens or stocks (skips interactive prompt)')
25
- .action(wrapAction(async (categoryArg, options) => {
24
+ .action(wrapAction(async (options) => {
26
25
  let category;
27
- const typeOpt = options?.type?.toLowerCase() || categoryArg?.toLowerCase();
26
+ const typeOpt = options?.type?.toLowerCase();
28
27
  if (typeOpt === 'tokens' || typeOpt === 'token') {
29
28
  category = 'tokens';
30
29
  }
@@ -9,36 +9,73 @@ import { printTxResult, printTable, LIMIT_ORDER_COLUMNS } from '../formatters.js
9
9
  // ─── create ──────────────────────────────────────────────────────────────
10
10
  const createCmd = new Command('create')
11
11
  .description('Create a limit order')
12
- .option('-y, --yes', 'Skip confirmation')
12
+ .option('-y, --yes', 'Skip transaction confirmation (Touch ID still required)')
13
+ .option('--chain <chain>', 'Blockchain (ethereum, base, solana, etc.)')
14
+ .option('--side <side>', 'buy or sell')
15
+ .option('--token <ticker|address>', 'Token symbol or contract address')
16
+ .option('--condition <condition>', 'Price condition (above or below)')
17
+ .option('--price <number>', 'Target price in USD')
18
+ .option('--amount <number>', 'Amount in USD')
19
+ .option('--expiry <hours>', 'Expiry time in hours')
13
20
  .action(wrapAction(async (opts) => {
14
21
  const creds = requireAuth();
15
- const chain = await selectChain('Chain:', true);
16
- const side = await select({
22
+ // Validate all flags upfront before any interactive prompts
23
+ if (opts.side && !['buy', 'sell'].includes(opts.side)) {
24
+ throw new Error(`Invalid side: ${opts.side}. Must be "buy" or "sell".`);
25
+ }
26
+ if (opts.condition && !['above', 'below'].includes(opts.condition)) {
27
+ throw new Error(`Invalid condition: ${opts.condition}. Must be "above" or "below".`);
28
+ }
29
+ if (opts.price !== undefined) {
30
+ const price = parseFloat(opts.price);
31
+ if (isNaN(price) || price <= 0) {
32
+ throw new Error('Target price must be a positive number.');
33
+ }
34
+ }
35
+ if (opts.amount !== undefined) {
36
+ const amountVal = parseFloat(opts.amount);
37
+ if (isNaN(amountVal) || amountVal <= 0) {
38
+ throw new Error('Amount must be a positive number.');
39
+ }
40
+ }
41
+ if (opts.expiry !== undefined) {
42
+ const expiryVal = parseFloat(opts.expiry);
43
+ if (isNaN(expiryVal) || expiryVal <= 0) {
44
+ throw new Error('Expiry must be a positive number of hours.');
45
+ }
46
+ }
47
+ const chain = opts.chain ?? await selectChain('Chain:', true);
48
+ const side = opts.side ?? await select({
17
49
  message: 'Side:',
18
50
  choices: [
19
51
  { name: 'Buy', value: 'buy' },
20
52
  { name: 'Sell', value: 'sell' },
21
53
  ],
22
54
  });
23
- const tokenInput = await input({
55
+ const tokenInput = opts.token ?? await input({
24
56
  message: 'Target token (contract address or ticker):',
25
57
  validate: (v) => (v.length > 0 ? true : 'Required'),
26
58
  });
27
59
  const tokenInfo = await lookupToken(tokenInput);
28
- const priceCondition = await select({
60
+ const priceCondition = opts.condition ?? await select({
29
61
  message: 'Trigger when price is:',
30
62
  choices: [
31
63
  { name: 'Above target price', value: 'above' },
32
64
  { name: 'Below target price', value: 'below' },
33
65
  ],
34
66
  });
35
- const targetPrice = await numberPrompt({ message: 'Target price (USD):', required: true });
36
- const amount = await input({
67
+ const targetPrice = opts.price ? parseFloat(opts.price) : await numberPrompt({ message: 'Target price (USD):', required: true });
68
+ const amountInput = opts.amount ?? await input({
37
69
  message: 'Amount (USD):',
38
70
  validate: (v) => (parseFloat(v) > 0 ? true : 'Enter positive number'),
39
71
  });
40
- const expireHours = await numberPrompt({ message: 'Expire after (hours):', default: 24 });
41
- const expiredAt = Math.floor(Date.now() / 1000) + (expireHours ?? 24) * 3600;
72
+ const amount = typeof amountInput === 'string' ? amountInput : String(amountInput);
73
+ const expireHoursRaw = opts.expiry ? parseFloat(opts.expiry) : await numberPrompt({ message: 'Expire after (hours):', default: 24 });
74
+ const expireHours = expireHoursRaw ?? 24;
75
+ if (isNaN(expireHours) || expireHours <= 0) {
76
+ throw new Error('Expiry must be a positive number of hours.');
77
+ }
78
+ const expiredAt = Math.floor(Date.now() / 1000) + expireHours * 3600;
42
79
  console.log('');
43
80
  console.log(chalk.bold('Limit Order:'));
44
81
  console.log(` Chain : ${chalk.cyan(chain)}`);
@@ -50,11 +87,8 @@ const createCmd = new Command('create')
50
87
  console.log(` Expires : ${new Date(expiredAt * 1000).toLocaleString()}`);
51
88
  console.log('');
52
89
  if (!opts.yes) {
53
- const ok = await confirm({ message: 'Create this limit order?', default: false });
54
- if (!ok)
55
- return;
90
+ await requireTransactionConfirmation(`Limit ${side} · $${amount} · price ${priceCondition} $${targetPrice} · ${chain}`, tokenInfo, { chain, side, amount: `$${amount}` });
56
91
  }
57
- await requireTransactionConfirmation(`Limit ${side} · $${amount} · price ${priceCondition} $${targetPrice} · ${chain}`, tokenInfo, { chain, side, amount: `$${amount}` });
58
92
  await requireTouchId();
59
93
  const spin = spinner('Creating limit order…');
60
94
  const res = await loApi.createLimitOrder(creds.accessToken, {
@@ -957,10 +957,11 @@ const closeCmd = new Command('close')
957
957
  success(`Position closed — ${sideLabel} ${symbol} ${sz}`);
958
958
  printTxResult(orderRes.data);
959
959
  }));
960
- // ─── leverage ────────────────────────────────────────────────────────────
961
960
  const leverageCmd = new Command('leverage')
962
961
  .description('Update leverage for a symbol')
963
962
  .option(WALLET_OPT[0], WALLET_OPT[1])
963
+ .option('-s, --symbol <TOKEN>', 'Target token symbol (e.g. ETH, SOL, BTC)')
964
+ .option('-l, --leverage <VALUE>', 'Leverage multiplier (e.g. 2, 3, 5, 10)')
964
965
  .action(wrapAction(async (opts) => {
965
966
  const creds = requireAuth();
966
967
  const resolved = await resolveWallet(creds.accessToken, opts.wallet, 'Update leverage on which wallet?');
@@ -970,8 +971,18 @@ const leverageCmd = new Command('leverage')
970
971
  const metaSpin = spinner('Fetching available assets…');
971
972
  const assets = await perpsApi.getAssetMeta();
972
973
  metaSpin.stop();
974
+ // Validate symbol if provided via CLI
973
975
  let symbol;
974
- if (assets.length > 0) {
976
+ if (opts.symbol) {
977
+ symbol = opts.symbol.toUpperCase();
978
+ const assetMeta = assets.find((a) => a.name.toUpperCase() === symbol);
979
+ if (!assetMeta) {
980
+ const validSymbols = assets.map((a) => a.name).join(', ');
981
+ console.error(chalk.red('✖'), `Invalid symbol: ${opts.symbol}. Supported: ${validSymbols}`);
982
+ process.exit(1);
983
+ }
984
+ }
985
+ else if (assets.length > 0) {
975
986
  symbol = await select({
976
987
  message: 'Asset:',
977
988
  choices: assets.map((a) => {
@@ -988,19 +999,42 @@ const leverageCmd = new Command('leverage')
988
999
  }
989
1000
  const meta = assets.find((a) => a.name.toUpperCase() === symbol.toUpperCase());
990
1001
  const maxLev = meta?.maxLeverage ?? 50;
991
- const leverage = await numberPrompt({
992
- message: `Leverage (1–${maxLev}x):`,
993
- min: 1,
994
- max: maxLev,
995
- required: true,
996
- });
997
- const isCross = await select({
998
- message: 'Margin mode:',
999
- choices: [
1000
- { name: 'Cross', value: true },
1001
- { name: 'Isolated', value: false },
1002
- ],
1003
- });
1002
+ // Validate and parse leverage
1003
+ let leverage;
1004
+ if (opts.leverage) {
1005
+ leverage = parseFloat(opts.leverage);
1006
+ if (isNaN(leverage) || leverage < 1) {
1007
+ console.error(chalk.red('✖'), `Invalid leverage: ${opts.leverage}. Must be a number >= 1.`);
1008
+ process.exit(1);
1009
+ }
1010
+ if (leverage > maxLev) {
1011
+ console.error(chalk.red(''), `Leverage ${leverage}x exceeds maximum ${maxLev}x for ${symbol}.`);
1012
+ process.exit(1);
1013
+ }
1014
+ }
1015
+ else {
1016
+ leverage = await numberPrompt({
1017
+ message: `Leverage (1–${maxLev}x):`,
1018
+ min: 1,
1019
+ max: maxLev,
1020
+ required: true,
1021
+ });
1022
+ }
1023
+ // Determine margin mode (cross vs isolated)
1024
+ // In non-interactive mode with both symbol and leverage, default to cross margin
1025
+ let isCross;
1026
+ if (opts.symbol && opts.leverage) {
1027
+ isCross = true;
1028
+ }
1029
+ else {
1030
+ isCross = await select({
1031
+ message: 'Margin mode:',
1032
+ choices: [
1033
+ { name: 'Cross', value: true },
1034
+ { name: 'Isolated', value: false },
1035
+ ],
1036
+ });
1037
+ }
1004
1038
  const spin = spinner('Updating leverage…');
1005
1039
  const res = await perpsApi.updateLeverage(creds.accessToken, { symbol, isCross, leverage: leverage, subAccountId: walletId });
1006
1040
  spin.stop();
@@ -288,81 +288,6 @@ async function handleCryptoCheckout(token, planId) {
288
288
  printKV(data);
289
289
  }
290
290
  }
291
- // ─── buy-credits ────────────────────────────────────────────────────────
292
- const buyCreditsCmd = new Command('buy-credits')
293
- .description('Buy a one-time credit package')
294
- .action(wrapAction(async () => {
295
- const creds = requireAuth();
296
- const spin = spinner('Fetching packages…');
297
- const plansRes = await paymentApi.getPlans();
298
- spin.stop();
299
- assertApiOk(plansRes, 'Failed to fetch packages');
300
- const { packages } = plansRes.data;
301
- if (packages.length === 0) {
302
- info('No credit packages available.');
303
- return;
304
- }
305
- // Select package
306
- const selectedPkgId = await select({
307
- message: 'Select a credit package:',
308
- choices: packages.map((pkg) => ({
309
- name: `$${pkg.amount} — ${Number(pkg.credit).toLocaleString()} credits`,
310
- value: pkg._id,
311
- })),
312
- });
313
- const selectedPkg = packages.find((p) => p._id === selectedPkgId);
314
- // Select payment method
315
- const payMethod = await select({
316
- message: 'Payment method:',
317
- choices: [
318
- { name: 'Credit Card (Stripe)', value: 'stripe' },
319
- { name: 'Crypto (USDC on-chain)', value: 'crypto' },
320
- ],
321
- });
322
- console.log('');
323
- console.log(chalk.bold('Package Summary:'));
324
- console.log(` Price : ${chalk.bold('$' + selectedPkg.amount)}`);
325
- console.log(` Credits : ${Number(selectedPkg.credit).toLocaleString()}`);
326
- console.log(` Payment : ${payMethod === 'stripe' ? 'Credit Card (Stripe)' : 'Crypto (USDC)'}`);
327
- console.log('');
328
- const ok = await confirm({ message: 'Proceed to checkout?', default: true });
329
- if (!ok) {
330
- console.log(chalk.dim('Cancelled.'));
331
- return;
332
- }
333
- if (payMethod === 'stripe') {
334
- const spin2 = spinner('Creating checkout session…');
335
- const res = await paymentApi.checkoutPackage(creds.accessToken, selectedPkgId, 'https://minara.ai/payment/success', 'https://minara.ai/payment/cancel');
336
- spin2.stop();
337
- assertApiOk(res, 'Failed to create checkout');
338
- const url = res.data?.url ?? res.data?.checkoutUrl;
339
- if (url) {
340
- success('Opening browser for payment…');
341
- console.log(chalk.cyan(` ${url}`));
342
- openBrowser(url);
343
- }
344
- else {
345
- success('Checkout created:');
346
- printKV(res.data);
347
- }
348
- }
349
- else {
350
- const spin2 = spinner('Creating crypto checkout…');
351
- const res = await paymentApi.cryptoCheckoutPackage(creds.accessToken, selectedPkgId);
352
- spin2.stop();
353
- assertApiOk(res, 'Failed to create crypto checkout');
354
- const url = res.data?.url ?? res.data?.checkoutUrl;
355
- if (url) {
356
- success('Opening browser for crypto payment…');
357
- console.log(chalk.cyan(` ${url}`));
358
- openBrowser(url);
359
- }
360
- else {
361
- success('Crypto checkout:');
362
- printKV(res.data);
363
- }
364
- }
365
- }));
366
291
  // ─── cancel ─────────────────────────────────────────────────────────────
367
292
  const cancelCmd = new Command('cancel')
368
293
  .description('Cancel your current subscription')
@@ -398,7 +323,6 @@ export const premiumCommand = new Command('premium')
398
323
  .addCommand(plansCmd)
399
324
  .addCommand(statusCmd)
400
325
  .addCommand(subscribeCmd)
401
- .addCommand(buyCreditsCmd)
402
326
  .addCommand(cancelCmd)
403
327
  .action(wrapAction(async () => {
404
328
  const action = await select({
@@ -407,7 +331,6 @@ export const premiumCommand = new Command('premium')
407
331
  { name: 'View available plans', value: 'plans' },
408
332
  { name: 'View my subscription', value: 'status' },
409
333
  { name: 'Subscribe / Change plan', value: 'subscribe' },
410
- { name: 'Buy credit package', value: 'buy-credits' },
411
334
  { name: 'Cancel subscription', value: 'cancel' },
412
335
  ],
413
336
  });
@@ -6,16 +6,24 @@ import { get } from '../api/client.js';
6
6
  import { requireAuth } from '../config.js';
7
7
  import { success, info, warn, spinner, assertApiOk, wrapAction, requireTransactionConfirmation, lookupToken, normalizeChain } from '../utils.js';
8
8
  import { requireTouchId } from '../touchid.js';
9
- import { printTxResult, printKV } from '../formatters.js';
9
+ import { printTxResult, printSwapSimulation } from '../formatters.js';
10
10
  export const swapCommand = new Command('swap')
11
11
  .description('Swap tokens (cross-chain spot trading)')
12
12
  .option('-s, --side <side>', 'buy or sell')
13
13
  .option('-t, --token <address|ticker>', 'Token contract address or ticker symbol')
14
14
  .option('-a, --amount <amount>', 'USD amount (buy) or token amount (sell)')
15
+ .option('-c, --chain <chain>', 'Blockchain (ethereum, base, solana, etc.)')
15
16
  .option('-y, --yes', 'Skip confirmation')
16
17
  .option('--dry-run', 'Simulate without executing')
17
18
  .action(wrapAction(async (opts) => {
18
19
  const creds = requireAuth();
20
+ // ── 0. Validate CLI options early ────────────────────────────────────
21
+ if (opts.amount) {
22
+ const amountNum = parseFloat(opts.amount);
23
+ if (opts.amount.toLowerCase() !== 'all' && (isNaN(amountNum) || amountNum <= 0)) {
24
+ throw new Error('Amount must be a positive number');
25
+ }
26
+ }
19
27
  // ── 1. Side ──────────────────────────────────────────────────────────
20
28
  let side = opts.side;
21
29
  if (!side) {
@@ -33,10 +41,17 @@ export const swapCommand = new Command('swap')
33
41
  validate: (v) => (v.length > 0 ? true : 'Please enter a token address or ticker'),
34
42
  });
35
43
  const tokenInfo = await lookupToken(tokenInput);
36
- // ── 3. Chain (derived from token) ────────────────────────────────────
37
- const chain = normalizeChain(tokenInfo.chain);
44
+ // ── 3. Chain (use explicit flag or derive from token) ────────────────
45
+ let chain = opts.chain ? normalizeChain(opts.chain) : undefined;
46
+ if (opts.chain && !chain) {
47
+ warn(`Unsupported chain: ${opts.chain}`);
48
+ return;
49
+ }
38
50
  if (!chain) {
39
- warn(`Unable to determine chain for token. Raw chain value: ${tokenInfo.chain ?? 'unknown'}`);
51
+ chain = normalizeChain(tokenInfo.chain);
52
+ }
53
+ if (!chain) {
54
+ warn('Unable to determine chain. Use --chain to specify.');
40
55
  return;
41
56
  }
42
57
  // ── 4. Amount ────────────────────────────────────────────────────────
@@ -89,17 +104,11 @@ export const swapCommand = new Command('swap')
89
104
  }]);
90
105
  spin.stop();
91
106
  assertApiOk(simRes, 'Simulation failed');
92
- console.log('');
93
- console.log(chalk.bold('Simulation Result:'));
94
- if (Array.isArray(simRes.data)) {
107
+ if (simRes.data && Array.isArray(simRes.data)) {
95
108
  for (const item of simRes.data) {
96
- printKV(item);
97
- console.log('');
109
+ printSwapSimulation(item);
98
110
  }
99
111
  }
100
- else if (simRes.data) {
101
- printKV(simRes.data);
102
- }
103
112
  return;
104
113
  }
105
114
  // ── 7. Confirm & Touch ID ──────────────────────────────────────────
@@ -15,6 +15,22 @@ export const transferCommand = new Command('transfer')
15
15
  .option('-y, --yes', 'Skip confirmation')
16
16
  .action(wrapAction(async (opts) => {
17
17
  const creds = requireAuth();
18
+ // ── 0. Validate CLI options early ────────────────────────────────────
19
+ if (opts.amount) {
20
+ const amountNum = parseFloat(opts.amount);
21
+ if (isNaN(amountNum) || amountNum <= 0) {
22
+ throw new Error('Amount must be a positive number');
23
+ }
24
+ }
25
+ if (opts.to) {
26
+ if (!opts.chain) {
27
+ throw new Error('--chain is required when --to is provided');
28
+ }
29
+ const addrValidation = validateAddress(opts.to, opts.chain);
30
+ if (addrValidation !== true) {
31
+ throw new Error(addrValidation);
32
+ }
33
+ }
18
34
  // ── 1. Chain ─────────────────────────────────────────────────────────
19
35
  const chain = opts.chain ?? await selectChain();
20
36
  // ── 2. Token ─────────────────────────────────────────────────────────
@@ -15,27 +15,45 @@ export const withdrawCommand = new Command('withdraw')
15
15
  .option('-y, --yes', 'Skip confirmation')
16
16
  .action(wrapAction(async (opts) => {
17
17
  const creds = requireAuth();
18
- // ── 1. Show current assets for reference ─────────────────────────────
19
- const assetsSpin = spinner('Fetching your assets…');
20
- const assetsRes = await getAssets(creds.accessToken);
21
- assetsSpin.stop();
22
- if (assetsRes.success && assetsRes.data) {
23
- const assets = assetsRes.data;
24
- if (Array.isArray(assets) && assets.length > 0) {
25
- console.log('');
26
- console.log(chalk.dim('Your current assets:'));
27
- for (const asset of assets.slice(0, 15)) {
28
- const sym = asset.symbol ?? asset.tokenSymbol ?? '';
29
- const bal = asset.balance ?? asset.amount ?? '';
30
- const ch = asset.chain ?? asset.chainName ?? '';
31
- if (sym || bal) {
32
- console.log(chalk.dim(` ${sym} ${bal} (${ch})`));
18
+ // ── 0. Validate CLI options early ────────────────────────────────────
19
+ if (opts.amount) {
20
+ const amountNum = parseFloat(opts.amount);
21
+ if (isNaN(amountNum) || amountNum <= 0) {
22
+ throw new Error('Amount must be a positive number');
23
+ }
24
+ }
25
+ if (opts.to) {
26
+ if (!opts.chain) {
27
+ throw new Error('--chain is required when --to is provided');
28
+ }
29
+ const addrValidation = validateAddress(opts.to, opts.chain);
30
+ if (addrValidation !== true) {
31
+ throw new Error(addrValidation);
32
+ }
33
+ }
34
+ // ── 1. Show current assets for reference (only in interactive mode)
35
+ if (!opts.chain && !opts.token && !opts.amount && !opts.to) {
36
+ const assetsSpin = spinner('Fetching your assets…');
37
+ const assetsRes = await getAssets(creds.accessToken);
38
+ assetsSpin.stop();
39
+ if (assetsRes.success && assetsRes.data) {
40
+ const assets = assetsRes.data;
41
+ if (Array.isArray(assets) && assets.length > 0) {
42
+ console.log('');
43
+ console.log(chalk.dim('Your current assets:'));
44
+ for (const asset of assets.slice(0, 15)) {
45
+ const sym = asset.symbol ?? asset.tokenSymbol ?? '';
46
+ const bal = asset.balance ?? asset.amount ?? '';
47
+ const ch = asset.chain ?? asset.chainName ?? '';
48
+ if (sym || bal) {
49
+ console.log(chalk.dim(` ${sym} ${bal} (${ch})`));
50
+ }
33
51
  }
52
+ if (assets.length > 15) {
53
+ console.log(chalk.dim(` … and ${assets.length - 15} more`));
54
+ }
55
+ console.log('');
34
56
  }
35
- if (assets.length > 15) {
36
- console.log(chalk.dim(` … and ${assets.length - 15} more`));
37
- }
38
- console.log('');
39
57
  }
40
58
  }
41
59
  // ── 2. Chain ─────────────────────────────────────────────────────────
@@ -1,3 +1,8 @@
1
+ import type { CrossChainSwapsSimulateResultDto, CrossChainSwapsSimulateErrorDto } from './types.js';
2
+ /** Convert wei (10^18) string to USD decimal string */
3
+ export declare function formatWeiToUsd(weiStr: string): string;
4
+ /** Convert BigInt amount string using token decimals */
5
+ export declare function formatTokenAmount(amountStr: string, decimals: number): string;
1
6
  /** Enable raw JSON output (all formatters fall back to JSON.stringify). */
2
7
  export declare function setRawJson(enabled: boolean): void;
3
8
  /** Check if raw JSON output mode is active. */
@@ -57,3 +62,7 @@ export declare function printFearGreed(data: Record<string, unknown>): void;
57
62
  * Pretty-print BTC/crypto metrics — flatten currentQuote, skip ohlcvQuotes.
58
63
  */
59
64
  export declare function printCryptoMetrics(data: Record<string, unknown>): void;
65
+ /** Check if a simulation result is an error */
66
+ export declare function isSimulateError(item: CrossChainSwapsSimulateResultDto | CrossChainSwapsSimulateErrorDto): item is CrossChainSwapsSimulateErrorDto;
67
+ /** Pretty-print swap simulation result with wei-to-USD conversion */
68
+ export declare function printSwapSimulation(result: CrossChainSwapsSimulateResultDto | CrossChainSwapsSimulateErrorDto): void;
@@ -5,6 +5,35 @@
5
5
  // ═══════════════════════════════════════════════════════════════════════════
6
6
  import chalk from 'chalk';
7
7
  import Table from 'cli-table3';
8
+ // ─── Wei / BigInt formatting ──────────────────────────────────────────────
9
+ /** Convert wei (10^18) string to USD decimal string */
10
+ export function formatWeiToUsd(weiStr) {
11
+ try {
12
+ const wei = BigInt(weiStr);
13
+ // Divide by 10^18 (wei to ether conversion)
14
+ const dollars = Number(wei) / 1e18;
15
+ return dollars.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 6 });
16
+ }
17
+ catch {
18
+ return weiStr; // fallback to original if parsing fails
19
+ }
20
+ }
21
+ /** Convert BigInt amount string using token decimals */
22
+ export function formatTokenAmount(amountStr, decimals) {
23
+ try {
24
+ const amount = BigInt(amountStr);
25
+ const value = Number(amount) / Math.pow(10, decimals);
26
+ return value.toLocaleString('en-US', { maximumFractionDigits: decimals });
27
+ }
28
+ catch {
29
+ return amountStr; // fallback to original if parsing fails
30
+ }
31
+ }
32
+ /** Check if a string looks like a wei value (large BigInt string) */
33
+ function isWeiString(value) {
34
+ // Wei strings are typically large numbers (18+ digits)
35
+ return /^\d{15,}$/.test(value);
36
+ }
8
37
  // ─── Raw JSON mode ───────────────────────────────────────────────────────
9
38
  let _rawJson = false;
10
39
  /** Enable raw JSON output (all formatters fall back to JSON.stringify). */
@@ -58,6 +87,10 @@ export function formatValue(value, key) {
58
87
  // Hex addresses — must check before numeric coercion (0x… is valid Number)
59
88
  if (/^0x[0-9a-fA-F]{20,}$/.test(value))
60
89
  return chalk.yellow(value);
90
+ // Wei-format USD fee strings (key contains "FeeInUsd" and value is large BigInt string)
91
+ if (key && /FeeInUsd$/i.test(key) && isWeiString(value)) {
92
+ return `$${formatWeiToUsd(value)}`;
93
+ }
61
94
  // Numeric string that looks like a price / amount
62
95
  const num = Number(value);
63
96
  if (!isNaN(num) && value.trim() !== '') {
@@ -452,6 +485,85 @@ export function printCryptoMetrics(data) {
452
485
  console.log(` ${chalk.dim(label.padEnd(maxLen))} : ${val}`);
453
486
  }
454
487
  }
488
+ // ─── Swap Simulation ─────────────────────────────────────────────────────
489
+ /** Check if a simulation result is an error */
490
+ export function isSimulateError(item) {
491
+ return 'error' in item;
492
+ }
493
+ /** Pretty-print swap simulation result with wei-to-USD conversion */
494
+ export function printSwapSimulation(result) {
495
+ if (_rawJson) {
496
+ console.log(JSON.stringify(result, null, 2));
497
+ return;
498
+ }
499
+ // Handle error case
500
+ if (isSimulateError(result)) {
501
+ console.log(chalk.red.bold(` Error: ${result.error}`));
502
+ if (result.message) {
503
+ console.log(chalk.dim(` Message: ${result.message}`));
504
+ }
505
+ return;
506
+ }
507
+ // Convert wei fees to USD (fees are always in 10^18 format)
508
+ const totalFeeUsd = formatWeiToUsd(result.totalFeeInUsd);
509
+ const gasFeeUsd = formatWeiToUsd(result.gasFeeInUsd);
510
+ const serviceFeeUsd = formatWeiToUsd(result.serviceFeeInUsd);
511
+ const lpFeeUsd = formatWeiToUsd(result.lpFeeInUsd);
512
+ // Sanity check: warn if total fee exceeds $1000
513
+ const totalFeeNum = parseFloat(totalFeeUsd.replace(/,/g, ''));
514
+ const feeWarning = totalFeeNum > 1000 ? chalk.yellow(' ⚠️ (unusually high)') : '';
515
+ // Print header
516
+ console.log('');
517
+ console.log(chalk.bold(' Simulation Result:'));
518
+ // Print token changes (using token.decimals for amount conversion)
519
+ if (result.increased.length > 0) {
520
+ console.log(chalk.green.bold(' Tokens Received:'));
521
+ for (const change of result.increased) {
522
+ const token = change.token;
523
+ const decimals = token.decimals ?? 18;
524
+ const amountFormatted = formatTokenAmount(change.amount, decimals);
525
+ const amountUsd = change.amountInUSD ? formatWeiToUsd(change.amountInUSD) : null;
526
+ console.log(` ${chalk.bold(`$${token.symbol}`)} — ${token.name}`);
527
+ console.log(` ${chalk.dim('Address')} : ${chalk.yellow(token.address)}`);
528
+ console.log(` ${chalk.dim('Amount')} : ${amountFormatted}`);
529
+ if (amountUsd)
530
+ console.log(` ${chalk.dim('Value')} : $${amountUsd}`);
531
+ }
532
+ }
533
+ if (result.decreased.length > 0) {
534
+ console.log(chalk.red.bold(' Tokens Spent:'));
535
+ for (const change of result.decreased) {
536
+ const token = change.token;
537
+ const decimals = token.decimals ?? 18;
538
+ const amountFormatted = formatTokenAmount(change.amount, decimals);
539
+ const amountUsd = change.amountInUSD ? formatWeiToUsd(change.amountInUSD) : null;
540
+ console.log(` ${chalk.bold(`$${token.symbol}`)} — ${token.name}`);
541
+ console.log(` ${chalk.dim('Address')} : ${chalk.yellow(token.address)}`);
542
+ console.log(` ${chalk.dim('Amount')} : ${amountFormatted}`);
543
+ if (amountUsd)
544
+ console.log(` ${chalk.dim('Value')} : $${amountUsd}`);
545
+ }
546
+ }
547
+ // Print fees
548
+ console.log('');
549
+ console.log(chalk.bold(' Fees:'));
550
+ console.log(` ${chalk.dim('Total Fee')} : $${totalFeeUsd}${feeWarning}`);
551
+ console.log(` ${chalk.dim('Gas Fee')} : $${gasFeeUsd}`);
552
+ console.log(` ${chalk.dim('Service Fee')} : $${serviceFeeUsd}`);
553
+ console.log(` ${chalk.dim('LP Fee')} : $${lpFeeUsd}`);
554
+ // Print other info
555
+ console.log('');
556
+ console.log(` ${chalk.dim('Slippage')} : ${result.slippageBps} bps`);
557
+ if (result.priceImpact !== null && result.priceImpact !== undefined) {
558
+ // -1 means price impact could not be calculated or is negligible
559
+ const impactStr = String(result.priceImpact).trim();
560
+ const impactNum = parseFloat(impactStr);
561
+ const impactDisplay = impactNum === -1 || impactStr === '-1'
562
+ ? chalk.dim('— (negligible)')
563
+ : `${impactStr}%`;
564
+ console.log(` ${chalk.dim('Price Impact')} : ${impactDisplay}`);
565
+ }
566
+ }
455
567
  // ─── Helpers ─────────────────────────────────────────────────────────────
456
568
  function truncate(str, len) {
457
569
  return str.length > len ? str.slice(0, len - 1) + '…' : str;
package/dist/types.d.ts CHANGED
@@ -123,6 +123,54 @@ export interface TransactionResult {
123
123
  status?: string;
124
124
  [key: string]: unknown;
125
125
  }
126
+ export interface TokenInfoDto {
127
+ name: string;
128
+ symbol: string;
129
+ address: string;
130
+ chainId: number;
131
+ decimals: number;
132
+ realDecimals: number;
133
+ price: number;
134
+ image?: string;
135
+ assetId?: string;
136
+ type?: string;
137
+ rank?: number | null;
138
+ slotIndex?: number | null;
139
+ }
140
+ export interface TokenChangeDto {
141
+ token: TokenInfoDto;
142
+ /** Amount of token changed (BigInt string, use token.decimals for conversion) */
143
+ amount: string;
144
+ /** Amount in USD (BigInt string in wei/10^18, may be null) */
145
+ amountInUSD: string | null;
146
+ }
147
+ export interface CrossChainSwapsSimulateResultDto {
148
+ /** Tokens that increased in balance */
149
+ increased: TokenChangeDto[];
150
+ /** Tokens that decreased in balance */
151
+ decreased: TokenChangeDto[];
152
+ /** Total fee in USD (BigInt string in wei/10^18) */
153
+ totalFeeInUsd: string;
154
+ /** Gas fee in USD (BigInt string in wei/10^18) */
155
+ gasFeeInUsd: string;
156
+ /** Service fee in USD (BigInt string in wei/10^18) */
157
+ serviceFeeInUsd: string;
158
+ /** LP fee in USD (BigInt string in wei/10^18) */
159
+ lpFeeInUsd: string;
160
+ /** Slippage in basis points */
161
+ slippageBps: number;
162
+ /** Price impact (may be null) */
163
+ priceImpact: string | null;
164
+ }
165
+ export interface CrossChainSwapsSimulateErrorDto {
166
+ error: string;
167
+ message?: string;
168
+ }
169
+ export type CrossChainSwapsSimulateItem = CrossChainSwapsSimulateResultDto | CrossChainSwapsSimulateErrorDto;
170
+ export interface CrossChainSwapsSimulateResponseDto {
171
+ success: boolean;
172
+ data: CrossChainSwapsSimulateItem[];
173
+ }
126
174
  export type SwapSide = 'buy' | 'sell';
127
175
  export interface CrossChainSwapDto {
128
176
  chain: Chain;
package/dist/utils.js CHANGED
@@ -211,11 +211,11 @@ export async function lookupToken(tokenInput) {
211
211
  const { searchTokens } = await import('./api/tokens.js');
212
212
  const res = await searchTokens(keyword);
213
213
  spin.stop();
214
- if (!res.success || !res.data || res.data.length === 0) {
215
- if (isTicker) {
216
- warn(`No token found for ticker $${keyword}`);
217
- }
218
- return { address: tokenInput };
214
+ if (!res.success) {
215
+ throw new Error(res.error?.message ?? 'Failed to lookup token');
216
+ }
217
+ if (!res.data || res.data.length === 0) {
218
+ throw new Error(`Unknown token: ${tokenInput}`);
219
219
  }
220
220
  let tokens = res.data;
221
221
  if (isTicker) {
@@ -276,9 +276,9 @@ export async function lookupToken(tokenInput) {
276
276
  chain: selected.chain,
277
277
  };
278
278
  }
279
- catch {
279
+ catch (err) {
280
280
  spin.stop();
281
- return { address: tokenInput };
281
+ throw err;
282
282
  }
283
283
  }
284
284
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "CLI client for Minara.ai — login, trade, deposit/withdraw, chat and more from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",