minara 0.2.0 → 0.2.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 CHANGED
@@ -18,9 +18,9 @@
18
18
 
19
19
  ## Features
20
20
 
21
- - **AI Chat** — Crypto-native AI for on-chain analysis, market research, and DeFi due diligence. Interactive REPL & single-shot queries with fast / quality / thinking modes
22
- - **Wallet** — Balances, deposits, and withdrawals across all supported chains
23
- - **Trading** — Cross-chain swaps, perpetual futures, limit orders, and copy trading. Accepts `$TICKER`, token name, or contract address
21
+ - **AI Chat** — Crypto-native AI for on-chain analysis, market research, and DeFi due diligence. Interactive REPL & single-shot queries with `fast` / `quality` / `thinking` modes
22
+ - **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits and withdrawals
23
+ - **Chain-Abstracted Trading** — Cross-chain swaps with automatic chain detection, perpetual futures, and limit orders. Accepts `$TICKER`, token name, or contract address
24
24
  - **Market Discovery** — Trending tokens, Fear & Greed Index, on-chain metrics, and token / stock search
25
25
 
26
26
  ## Installation
@@ -55,8 +55,8 @@ minara chat
55
55
  # Or send a single question
56
56
  minara chat "What's the best DeFi yield right now?"
57
57
 
58
- # Swap tokens (accepts ticker or address)
59
- minara swap -c solana -t '$BONK' -s buy -a 100
58
+ # Swap tokens (chain auto-detected from token)
59
+ minara swap -t '$BONK' -s buy -a 100
60
60
 
61
61
  # View trending tokens
62
62
  minara discover trending
@@ -81,19 +81,23 @@ minara login --apple # Apple ID (opens browser)
81
81
 
82
82
  ### Wallet & Funds
83
83
 
84
- | Command | Description |
85
- | --------------------- | ----------------------------------------------------- |
86
- | `minara assets` | View wallet assets (interactive: spot / perps / both) |
87
- | `minara assets spot` | View spot wallet balances across all chains |
88
- | `minara assets perps` | View perps account balance and open positions |
89
- | `minara deposit` | Show deposit addresses and supported networks |
90
- | `minara withdraw` | Withdraw tokens to an external wallet |
84
+ | Command | Description |
85
+ | --------------------- | ----------------------------------------------- |
86
+ | `minara balance` | Combined USDC/USDT balance across spot and perps |
87
+ | `minara assets` | Full overview: spot holdings + perps account |
88
+ | `minara assets spot` | Spot wallet: portfolio value, cost, PnL, holdings |
89
+ | `minara assets perps` | Perps account: equity, margin, positions |
90
+ | `minara deposit` | Deposit to spot (view addresses) or perps (direct / from spot) |
91
+ | `minara withdraw` | Withdraw tokens to an external wallet |
91
92
 
92
93
  ```bash
93
- minara assets # Interactive: Spot / Perps / Both
94
- minara assets spot # Spot wallet across all chains
95
- minara assets perps # Perps account balance + positions
96
- minara deposit
94
+ minara balance # Quick total: Spot + Perps available balance
95
+ minara assets # Full overview (spot + perps)
96
+ minara assets spot # Spot wallet with PnL breakdown
97
+ minara assets perps # Perps equity, margin, positions
98
+ minara deposit # Interactive: Spot (addresses) or Perps (address / transfer)
99
+ minara deposit spot # Show spot wallet deposit addresses (EVM + Solana)
100
+ minara deposit perps # Perps: show Arbitrum deposit address, or transfer from Spot → Perps
97
101
  minara withdraw -c solana -t '$SOL' -a 10 --to <address>
98
102
  minara withdraw # Interactive mode (accepts ticker or address)
99
103
  ```
@@ -102,23 +106,25 @@ minara withdraw # Interactive mode (accepts ticker or address)
102
106
 
103
107
  | Command | Description |
104
108
  | ----------------- | ---------------------------------- |
105
- | `minara swap` | Swap tokens (cross-chain) |
109
+ | `minara swap` | Swap tokens (chain auto-detected) |
106
110
  | `minara transfer` | Transfer tokens to another address |
107
111
 
108
112
  ```bash
109
- minara swap # Interactive
110
- minara swap -c solana -s buy -t '$BONK' -a 100 # By ticker
111
- minara swap -c solana -s buy -t <address> -a 100 # By contract address
113
+ minara swap # Interactive: side → token → amount
114
+ minara swap -s buy -t '$BONK' -a 100 # Buy by ticker (chain auto-detected)
115
+ minara swap -s sell -t '$NVDAx' -a all # Sell entire balance
112
116
  minara swap --dry-run # Simulate without executing
113
117
  ```
114
118
 
115
- > **Token input:** All token fields (`-t`) accept a `$TICKER` (e.g. `$BONK`), a token name, or a contract address. When multiple tokens match, you'll be prompted to select the correct one with full contract addresses displayed.
119
+ > **Chain abstraction:** The chain is automatically detected from the token. If a token exists on multiple chains (e.g. USDC), you'll be prompted to pick one, sorted by gas cost (lowest first). Sell mode supports `all` to sell full balance, and caps amounts exceeding your balance.
120
+ >
121
+ > **Token input:** All token fields (`-t`) accept a `$TICKER` (e.g. `$BONK`), a token name, or a contract address.
116
122
 
117
123
  ### Perpetual Futures
118
124
 
119
125
  | Command | Description |
120
126
  | --------------------------- | ----------------------------------------- |
121
- | `minara perps deposit` | Deposit USDC to Hyperliquid perps account |
127
+ | `minara perps deposit` | Deposit USDC to perps (or use `minara deposit perps`) |
122
128
  | `minara perps withdraw` | Withdraw USDC from perps account |
123
129
  | `minara perps positions` | View all open positions |
124
130
  | `minara perps order` | Place an order (interactive builder) |
@@ -149,23 +155,6 @@ minara limit-order list # Show all orders with status
149
155
  minara limit-order cancel abc123 # Cancel order by ID
150
156
  ```
151
157
 
152
- ### Copy Trading
153
-
154
- | Command | Description |
155
- | ------------------------------- | --------------------------- |
156
- | `minara copy-trade create` | Create a new copy-trade bot |
157
- | `minara copy-trade list` | List all copy-trade bots |
158
- | `minara copy-trade start <id>` | Start a paused bot |
159
- | `minara copy-trade stop <id>` | Pause a running bot |
160
- | `minara copy-trade delete <id>` | Delete a bot permanently |
161
-
162
- ```bash
163
- minara copy-trade create # Interactive: target wallet, chain, amount, options
164
- minara copy-trade list # Show all bots with status
165
- minara copy-trade start abc123 # Resume a paused bot
166
- minara copy-trade stop abc123 # Pause a running bot
167
- ```
168
-
169
158
  ### AI Chat
170
159
 
171
160
  | Command | Description |
@@ -279,12 +268,15 @@ All fund-related operations go through a multi-layer safety flow:
279
268
  4. Execute
280
269
  ```
281
270
 
282
- The **transaction confirmation** shows the token ticker, name, full contract address, and operation details before asking for final approval:
271
+ The **transaction confirmation** shows chain, token, address, side, amount, and operation details before asking for final approval:
283
272
 
284
273
  ```
285
274
  ⚠ Transaction confirmation
275
+ Chain : solana
286
276
  Token : $BONK — Bonk
287
277
  Address : DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263
278
+ Side : BUY
279
+ Amount : 100 USD
288
280
  Action : BUY swap · 100 USD · solana
289
281
  ? Are you sure you want to proceed? (y/N)
290
282
  ```
@@ -299,7 +291,7 @@ Minara CLI supports macOS Touch ID to protect all fund-related operations. When
299
291
  minara config # Select "Touch ID" to enable / disable
300
292
  ```
301
293
 
302
- **Protected operations:** `withdraw`, `transfer`, `swap`, `perps deposit`, `perps withdraw`, `perps order`, `limit-order create`, `copy-trade create`
294
+ **Protected operations:** `withdraw`, `transfer`, `swap`, `deposit` (Spot→Perps transfer), `perps deposit`, `perps withdraw`, `perps order`, `limit-order create`
303
295
 
304
296
  > **Note:** Touch ID requires macOS with Touch ID hardware. The `--yes` flag skips the initial confirmation prompt but does **not** bypass transaction confirmation or Touch ID.
305
297
 
@@ -11,8 +11,6 @@ export declare function cancelOrders(token: string, dto: PerpsCancelOrdersDto):
11
11
  export declare function modifyOrders(token: string, dto: PerpsCancelOrdersDto): Promise<import("../types.js").ApiResponse<TransactionResult>>;
12
12
  /** Update leverage */
13
13
  export declare function updateLeverage(token: string, dto: UpdateLeverageDto): Promise<import("../types.js").ApiResponse<void>>;
14
- /** Get perps account state (balance, equity, margin) */
15
- export declare function getAccountState(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
16
14
  /** Get all positions */
17
15
  export declare function getPositions(token: string): Promise<import("../types.js").ApiResponse<PerpsPosition[]>>;
18
16
  /** Get completed trades */
@@ -23,6 +21,8 @@ export declare function getTokenPrices(token: string): Promise<import("../types.
23
21
  export declare function getFundRecords(token: string, page: number, limit: number): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
24
22
  /** Get equity history chart */
25
23
  export declare function getEquityHistory(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
24
+ /** Get perps account summary (balance, equity, positions, PnL) */
25
+ export declare function getAccountSummary(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
26
26
  /** Get all decisions */
27
27
  export declare function getDecisions(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
28
28
  /** Claim rewards */
package/dist/api/perps.js CHANGED
@@ -23,10 +23,6 @@ export function modifyOrders(token, dto) {
23
23
  export function updateLeverage(token, dto) {
24
24
  return post('/v1/tx/perps/update-leverage', { token, body: dto });
25
25
  }
26
- /** Get perps account state (balance, equity, margin) */
27
- export function getAccountState(token) {
28
- return get('/v1/tx/perps/account-state', { token });
29
- }
30
26
  /** Get all positions */
31
27
  export function getPositions(token) {
32
28
  return get('/v1/tx/perps/positions/all', { token });
@@ -47,6 +43,10 @@ export function getFundRecords(token, page, limit) {
47
43
  export function getEquityHistory(token) {
48
44
  return get('/v1/tx/perps/equity-history-chart/all', { token });
49
45
  }
46
+ /** Get perps account summary (balance, equity, positions, PnL) */
47
+ export function getAccountSummary(token) {
48
+ return get('/v1/fully-managed/account-summary', { token });
49
+ }
50
50
  /** Get all decisions */
51
51
  export function getDecisions(token) {
52
52
  return get('/v1/tx/perps/decisions/all', { token });
@@ -1,11 +1,10 @@
1
1
  import { Command } from 'commander';
2
- import { select } from '@inquirer/prompts';
3
2
  import chalk from 'chalk';
4
- import { getAssets } from '../api/crosschain.js';
5
3
  import * as perpsApi from '../api/perps.js';
4
+ import { get } from '../api/client.js';
6
5
  import { requireAuth } from '../config.js';
7
- import { spinner, unwrapApi, wrapAction } from '../utils.js';
8
- import { printKV, printTable, ASSET_COLUMNS, POSITION_COLUMNS } from '../formatters.js';
6
+ import { spinner, wrapAction } from '../utils.js';
7
+ import { printTable, SPOT_COLUMNS, POSITION_COLUMNS } from '../formatters.js';
9
8
  // ─── spot ────────────────────────────────────────────────────────────────
10
9
  const spotCmd = new Command('spot')
11
10
  .description('View spot wallet assets across chains')
@@ -13,22 +12,62 @@ const spotCmd = new Command('spot')
13
12
  const creds = requireAuth();
14
13
  await showSpotAssets(creds.accessToken);
15
14
  }));
15
+ const MIN_DISPLAY_VALUE = 0.01;
16
16
  async function showSpotAssets(token) {
17
17
  const spin = spinner('Fetching spot assets…');
18
- const res = await getAssets(token);
18
+ const res = await get('/users/pnls/all', { token });
19
19
  spin.stop();
20
- const data = unwrapApi(res, 'Failed to fetch spot assets');
21
- if (Array.isArray(data) && data.length === 0) {
22
- console.log(chalk.dim('No spot assets found.'));
20
+ if (!res.success || !res.data) {
21
+ console.log('');
22
+ console.log(chalk.bold('Spot Wallet Assets:'));
23
+ console.log(chalk.dim(' Could not fetch spot assets.'));
24
+ if (res.error?.message)
25
+ console.log(chalk.dim(` ${res.error.message}`));
26
+ console.log('');
23
27
  return;
24
28
  }
29
+ const all = res.data;
30
+ const holdings = [];
31
+ let totalValue = 0;
32
+ let totalRealizedPnl = 0;
33
+ let totalUnrealizedPnl = 0;
34
+ let hasUnrealizedPnl = false;
35
+ for (const t of all) {
36
+ const bal = Number(t.balance ?? 0);
37
+ const price = Number(t.marketPrice ?? 0);
38
+ const apiVal = Number(t.portfolioValue ?? 0);
39
+ const value = apiVal > 0 ? apiVal : bal * price;
40
+ const uPnl = Number(t.unrealizedPnl ?? 0);
41
+ const rPnl = Number(t.realizedPnl ?? 0);
42
+ totalValue += value;
43
+ totalRealizedPnl += rPnl;
44
+ if (uPnl !== 0) {
45
+ totalUnrealizedPnl += uPnl;
46
+ hasUnrealizedPnl = true;
47
+ }
48
+ if (bal > 0 && value >= MIN_DISPLAY_VALUE) {
49
+ holdings.push({ ...t, _value: value });
50
+ }
51
+ }
52
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
53
+ const pnlFmt = (n) => {
54
+ if (n === 0)
55
+ return chalk.dim('$0.00');
56
+ const color = n >= 0 ? chalk.green : chalk.red;
57
+ return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
58
+ };
25
59
  console.log('');
26
- console.log(chalk.bold('Spot Wallet Assets:'));
27
- if (Array.isArray(data)) {
28
- printTable(data, ASSET_COLUMNS);
60
+ console.log(chalk.bold('Spot Wallet:'));
61
+ console.log(` Portfolio Value : ${fmt(totalValue)}`);
62
+ console.log(` Unrealized PnL : ${pnlFmt(totalUnrealizedPnl)}`);
63
+ console.log(` Realized PnL : ${pnlFmt(totalRealizedPnl)}`);
64
+ console.log('');
65
+ console.log(chalk.bold(`Holdings (${holdings.length}):`));
66
+ if (holdings.length === 0) {
67
+ console.log(chalk.dim(' No spot assets with balance.'));
29
68
  }
30
69
  else {
31
- printKV(data);
70
+ printTable(holdings, SPOT_COLUMNS);
32
71
  }
33
72
  console.log('');
34
73
  }
@@ -40,42 +79,38 @@ const perpsCmd = new Command('perps')
40
79
  await showPerpsAssets(creds.accessToken);
41
80
  }));
42
81
  async function showPerpsAssets(token) {
43
- // Fetch account state and positions in parallel
44
82
  const spin = spinner('Fetching perps account…');
45
- const [accountRes, positionsRes] = await Promise.all([
46
- perpsApi.getAccountState(token),
47
- perpsApi.getPositions(token),
48
- ]);
83
+ const res = await perpsApi.getAccountSummary(token);
49
84
  spin.stop();
50
- // ── Account state ───────────────────────────────────────────────────
85
+ if (!res.success || !res.data) {
86
+ console.log(chalk.dim(' Could not fetch perps account.'));
87
+ if (res.error?.message)
88
+ console.log(chalk.dim(` ${res.error.message}`));
89
+ return;
90
+ }
91
+ const d = res.data;
92
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
93
+ const pnlFmt = (n) => {
94
+ const color = n >= 0 ? chalk.green : chalk.red;
95
+ return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
96
+ };
97
+ // ── Account overview ───────────────────────────────────────────────
51
98
  console.log('');
52
99
  console.log(chalk.bold('Perps Account:'));
53
- if (accountRes.success && accountRes.data) {
54
- printKV(accountRes.data);
55
- }
56
- else {
57
- console.log(chalk.dim(' Could not fetch account state.'));
58
- if (accountRes.error?.message) {
59
- console.log(chalk.dim(` ${accountRes.error.message}`));
60
- }
61
- }
100
+ console.log(` Equity : ${fmt(Number(d.equityValue ?? 0))}`);
101
+ console.log(` Available : ${fmt(Number(d.dispatchableValue ?? 0))}`);
102
+ console.log(` Margin Used : ${fmt(Number(d.totalMarginUsed ?? 0))}`);
103
+ console.log(` Unrealized PnL: ${pnlFmt(Number(d.totalUnrealizedPnl ?? 0))}`);
104
+ console.log(` Withdrawable : ${fmt(Number(d.withdrawableValue ?? 0))}`);
62
105
  // ── Positions ───────────────────────────────────────────────────────
106
+ const positions = Array.isArray(d.positions) ? d.positions : [];
63
107
  console.log('');
64
- console.log(chalk.bold('Open Positions:'));
65
- if (!positionsRes.success) {
66
- console.log(chalk.dim(' Could not fetch positions.'));
67
- if (positionsRes.error?.message) {
68
- console.log(chalk.dim(` ${positionsRes.error.message}`));
69
- }
108
+ console.log(chalk.bold(`Open Positions (${positions.length}):`));
109
+ if (positions.length === 0) {
110
+ console.log(chalk.dim(' No open positions.'));
70
111
  }
71
112
  else {
72
- const positions = positionsRes.data;
73
- if (!positions || (Array.isArray(positions) && positions.length === 0)) {
74
- console.log(chalk.dim(' No open positions.'));
75
- }
76
- else {
77
- printTable(positions, POSITION_COLUMNS);
78
- }
113
+ printTable(positions, POSITION_COLUMNS);
79
114
  }
80
115
  console.log('');
81
116
  }
@@ -85,19 +120,7 @@ export const assetsCommand = new Command('assets')
85
120
  .addCommand(spotCmd)
86
121
  .addCommand(perpsCmd)
87
122
  .action(wrapAction(async () => {
88
- const action = await select({
89
- message: 'View assets:',
90
- choices: [
91
- { name: 'Spot wallet', value: 'spot' },
92
- { name: 'Perps account', value: 'perps' },
93
- { name: 'Both', value: 'both' },
94
- ],
95
- });
96
123
  const creds = requireAuth();
97
- if (action === 'spot' || action === 'both') {
98
- await showSpotAssets(creds.accessToken);
99
- }
100
- if (action === 'perps' || action === 'both') {
101
- await showPerpsAssets(creds.accessToken);
102
- }
124
+ await showSpotAssets(creds.accessToken);
125
+ await showPerpsAssets(creds.accessToken);
103
126
  }));
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const balanceCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { get } from '../api/client.js';
4
+ import * as perpsApi from '../api/perps.js';
5
+ import { requireAuth } from '../config.js';
6
+ import { spinner, wrapAction } from '../utils.js';
7
+ const STABLES = new Set(['usdc', 'usdt']);
8
+ export const balanceCommand = new Command('balance')
9
+ .description('Show combined USDC / USDT balance across spot and perps')
10
+ .action(wrapAction(async () => {
11
+ const creds = requireAuth();
12
+ const spin = spinner('Fetching balances…');
13
+ const [spotRes, perpsRes] = await Promise.all([
14
+ get('/users/pnls/all', { token: creds.accessToken }),
15
+ perpsApi.getAccountSummary(creds.accessToken),
16
+ ]);
17
+ spin.stop();
18
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
19
+ let spotStable = 0;
20
+ if (spotRes.success && Array.isArray(spotRes.data)) {
21
+ for (const t of spotRes.data) {
22
+ const sym = String(t.tokenSymbol ?? '').toLowerCase();
23
+ if (STABLES.has(sym)) {
24
+ const bal = Number(t.balance ?? 0);
25
+ const price = Number(t.marketPrice ?? 1);
26
+ spotStable += bal * price;
27
+ }
28
+ }
29
+ }
30
+ let perpsAvailable = 0;
31
+ if (perpsRes.success && perpsRes.data) {
32
+ const d = perpsRes.data;
33
+ perpsAvailable = Number(d.dispatchableValue ?? 0);
34
+ }
35
+ const total = spotStable + perpsAvailable;
36
+ console.log('');
37
+ console.log(chalk.bold('Balance:'));
38
+ console.log(` Spot (USDC/USDT) : ${fmt(spotStable)}`);
39
+ console.log(` Perps (available) : ${fmt(perpsAvailable)}`);
40
+ console.log(` ${'─'.repeat(30)}`);
41
+ console.log(` Total : ${chalk.bold(fmt(total))}`);
42
+ console.log('');
43
+ }));
@@ -1,83 +1,154 @@
1
1
  import { Command } from 'commander';
2
- import { select } from '@inquirer/prompts';
2
+ import { select, confirm, number as numberPrompt } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
- import Table from 'cli-table3';
5
- import { getCurrentUser } from '../api/auth.js';
6
4
  import { getAccount } from '../api/crosschain.js';
5
+ import { getCurrentUser } from '../api/auth.js';
6
+ import * as perpsApi from '../api/perps.js';
7
7
  import { requireAuth } from '../config.js';
8
- import { info, spinner, unwrapApi, wrapAction } from '../utils.js';
9
- import { printKV } from '../formatters.js';
10
- /**
11
- * Map wallet type keys from /auth/me human-readable chain info.
12
- */
13
- function describeWalletType(key) {
14
- const lower = key.toLowerCase();
15
- if (lower.includes('evm') || lower === 'spot-evm' || lower === 'abstraction-evm') {
16
- return {
17
- network: 'EVM',
18
- chains: ['Ethereum', 'Base', 'Arbitrum', 'Optimism', 'Polygon', 'Avalanche', 'BSC', 'Berachain', 'Blast'],
19
- };
20
- }
21
- if (lower.includes('solana')) {
22
- return { network: 'Solana', chains: ['Solana'] };
23
- }
24
- return { network: key, chains: [key] };
25
- }
26
- export const depositCommand = new Command('deposit')
27
- .description('Show your deposit addresses and supported networks')
8
+ import { info, success, spinner, assertApiOk, wrapAction, requireTransactionConfirmation } from '../utils.js';
9
+ import { requireTouchId } from '../touchid.js';
10
+ import { printTxResult } from '../formatters.js';
11
+ const EVM_CHAINS = 'Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche, BSC, Berachain, Blast';
12
+ // ─── spot ────────────────────────────────────────────────────────────────
13
+ const spotCmd = new Command('spot')
14
+ .description('Show spot wallet deposit addresses')
28
15
  .action(wrapAction(async () => {
29
16
  const creds = requireAuth();
17
+ await showSpotDeposit(creds.accessToken);
18
+ }));
19
+ async function showSpotDeposit(token) {
30
20
  const spin = spinner('Fetching deposit addresses…');
31
- const [userRes, accountRes] = await Promise.all([
32
- getCurrentUser(creds.accessToken),
33
- getAccount(creds.accessToken),
34
- ]);
21
+ const res = await getAccount(token);
35
22
  spin.stop();
36
- const user = unwrapApi(userRes, 'Failed to fetch account info');
37
- const wallets = user.wallets;
38
- if (!wallets || Object.keys(wallets).length === 0) {
39
- info('No wallet addresses found. Your account may not have been fully initialized.');
40
- info('Try logging in at https://minara.ai first, then run this command again.');
23
+ if (!res.success || !res.data) {
24
+ info('Could not fetch deposit addresses. Try logging in at https://minara.ai first.');
25
+ return;
26
+ }
27
+ const data = res.data;
28
+ const evmAddr = data.evmAddress;
29
+ const solAddr = data.solanaAddress;
30
+ if (!evmAddr && !solAddr) {
31
+ info('No deposit addresses found. Your account may not have been fully initialized.');
41
32
  return;
42
33
  }
43
34
  console.log('');
44
- console.log(chalk.bold('Deposit Addresses'));
35
+ console.log(chalk.bold('Spot Deposit Addresses'));
45
36
  console.log(chalk.dim('Send tokens to the addresses below. Make sure to use the correct network!'));
46
37
  console.log('');
47
- const table = new Table({
48
- head: [chalk.white('Network'), chalk.white('Address'), chalk.white('Supported Chains')],
49
- colWidths: [14, 48, 40],
50
- wordWrap: true,
51
- });
52
- const seen = new Set();
53
- for (const [walletType, address] of Object.entries(wallets)) {
54
- if (!address || seen.has(address))
55
- continue;
56
- seen.add(address);
57
- const { network, chains } = describeWalletType(walletType);
58
- table.push([chalk.cyan.bold(network), chalk.yellow(address), chains.join(', ')]);
38
+ if (solAddr) {
39
+ console.log(` ${chalk.cyan.bold('Solana')}`);
40
+ console.log(` Address : ${chalk.yellow(solAddr)}`);
41
+ console.log(` Chains : Solana`);
42
+ console.log('');
43
+ }
44
+ if (evmAddr) {
45
+ console.log(` ${chalk.cyan.bold('EVM')}`);
46
+ console.log(` Address : ${chalk.yellow(evmAddr)}`);
47
+ console.log(` Chains : ${EVM_CHAINS}`);
48
+ console.log('');
59
49
  }
60
- console.log(table.toString());
61
- console.log('');
62
50
  console.log(chalk.red.bold('Important:'));
63
51
  console.log(chalk.red(' • Only send tokens on the supported chains listed above.'));
64
52
  console.log(chalk.red(' • Sending tokens on the wrong network may result in permanent loss.'));
65
- console.log(chalk.red(' • EVM address supports all EVM-compatible chains (Ethereum, Base, Arbitrum, etc.)'));
66
53
  console.log('');
67
- if (accountRes.success && accountRes.data) {
68
- const wantDetails = await select({
69
- message: 'Would you like to see detailed account info?',
70
- choices: [
71
- { name: 'Yes', value: true },
72
- { name: 'No', value: false },
73
- ],
74
- default: false,
75
- });
76
- if (wantDetails) {
77
- console.log('');
78
- console.log(chalk.bold('Account Details:'));
79
- printKV(accountRes.data);
80
- console.log('');
81
- }
54
+ }
55
+ // ─── perps ───────────────────────────────────────────────────────────────
56
+ const perpsCmd = new Command('perps')
57
+ .description('Deposit USDC to perps account')
58
+ .option('-a, --amount <amount>', 'USDC amount (for transfer)')
59
+ .option('-y, --yes', 'Skip confirmation')
60
+ .action(wrapAction(async (opts) => {
61
+ const creds = requireAuth();
62
+ await perpsDepositFlow(creds.accessToken, opts);
63
+ }));
64
+ async function perpsDepositFlow(token, opts) {
65
+ const method = await select({
66
+ message: 'How would you like to deposit to perps?',
67
+ choices: [
68
+ { name: 'Show perps deposit address (for external transfers)', value: 'address' },
69
+ { name: `${chalk.bold('Transfer from Spot wallet → Perps wallet')} (internal)`, value: 'transfer' },
70
+ ],
71
+ });
72
+ if (method === 'address') {
73
+ await showPerpsDepositAddresses(token);
74
+ }
75
+ else {
76
+ await transferSpotToPerps(token, opts);
77
+ }
78
+ }
79
+ async function showPerpsDepositAddresses(token) {
80
+ const spin = spinner('Fetching perps deposit addresses…');
81
+ const res = await getCurrentUser(token);
82
+ spin.stop();
83
+ if (!res.success || !res.data) {
84
+ info('Could not fetch perps addresses. Try logging in at https://minara.ai first.');
85
+ return;
86
+ }
87
+ const wallets = res.data.wallets ?? {};
88
+ const perpsEvm = wallets['perpetual-evm'];
89
+ if (!perpsEvm) {
90
+ info('No perps deposit address found. Your perps account may not have been initialized yet.');
91
+ return;
92
+ }
93
+ console.log('');
94
+ console.log(chalk.bold('Perps Deposit Address'));
95
+ console.log(chalk.dim('Send USDC to the address below to fund your perps account directly.'));
96
+ console.log('');
97
+ console.log(` ${chalk.cyan.bold('EVM (Arbitrum)')}`);
98
+ console.log(` Address : ${chalk.yellow(perpsEvm)}`);
99
+ console.log('');
100
+ console.log(chalk.red.bold('Important:'));
101
+ console.log(chalk.red(' • Only send USDC on Arbitrum to this address.'));
102
+ console.log(chalk.red(' • Sending other tokens or using the wrong network may result in permanent loss.'));
103
+ console.log('');
104
+ }
105
+ async function transferSpotToPerps(token, opts) {
106
+ console.log('');
107
+ console.log(chalk.yellow.bold('⚠ This will transfer USDC from your Spot wallet to your Perps wallet.'));
108
+ console.log('');
109
+ const amount = opts?.amount
110
+ ? parseFloat(opts.amount)
111
+ : await numberPrompt({ message: 'USDC amount to transfer from Spot → Perps (min 5):', min: 5, required: true });
112
+ if (!amount || amount < 5) {
113
+ console.error(chalk.red('✖'), 'Minimum deposit is 5 USDC');
114
+ process.exit(1);
115
+ }
116
+ console.log(`\n Transfer : ${chalk.bold(amount)} USDC ${chalk.dim('Spot wallet')} → ${chalk.cyan('Perps wallet')}\n`);
117
+ if (!opts?.yes) {
118
+ const ok = await confirm({ message: 'Confirm transfer from Spot to Perps?', default: true });
119
+ if (!ok)
120
+ return;
121
+ }
122
+ await requireTransactionConfirmation(`Transfer ${amount} USDC from Spot → Perps`, undefined, {
123
+ amount: `${amount} USDC`,
124
+ side: 'Spot → Perps',
125
+ });
126
+ await requireTouchId();
127
+ const spin = spinner('Transferring…');
128
+ const res = await perpsApi.deposit(token, { usdcAmount: amount });
129
+ spin.stop();
130
+ assertApiOk(res, 'Transfer failed');
131
+ success(`Transferred ${amount} USDC from Spot wallet to Perps wallet`);
132
+ printTxResult(res.data);
133
+ }
134
+ // ─── parent ──────────────────────────────────────────────────────────────
135
+ export const depositCommand = new Command('deposit')
136
+ .description('Deposit to spot wallet or perps account')
137
+ .addCommand(spotCmd)
138
+ .addCommand(perpsCmd)
139
+ .action(wrapAction(async () => {
140
+ const action = await select({
141
+ message: 'Deposit to:',
142
+ choices: [
143
+ { name: 'Spot wallet — view deposit addresses', value: 'spot' },
144
+ { name: 'Perps wallet — view deposit address or transfer from Spot', value: 'perps' },
145
+ ],
146
+ });
147
+ const creds = requireAuth();
148
+ if (action === 'spot') {
149
+ await showSpotDeposit(creds.accessToken);
150
+ }
151
+ else {
152
+ await perpsDepositFlow(creds.accessToken);
82
153
  }
83
154
  }));
@@ -54,7 +54,7 @@ const createCmd = new Command('create')
54
54
  if (!ok)
55
55
  return;
56
56
  }
57
- await requireTransactionConfirmation(`Limit ${side} · $${amount} · price ${priceCondition} $${targetPrice} · ${chain}`, tokenInfo);
57
+ await requireTransactionConfirmation(`Limit ${side} · $${amount} · price ${priceCondition} $${targetPrice} · ${chain}`, tokenInfo, { chain, side, amount: `$${amount}` });
58
58
  await requireTouchId();
59
59
  const spin = spinner('Creating limit order…');
60
60
  const res = await loApi.createLimitOrder(creds.accessToken, {
@@ -124,6 +124,7 @@ const statusCmd = new Command('status')
124
124
  spin.stop();
125
125
  assertApiOk(userRes, 'Failed to fetch account info');
126
126
  const user = userRes.data;
127
+ const plans = plansRes.success && plansRes.data ? plansRes.data.plans : [];
127
128
  if (isRawJson()) {
128
129
  console.log(JSON.stringify({
129
130
  subscription: user.subscription ?? null,
@@ -134,18 +135,37 @@ const statusCmd = new Command('status')
134
135
  console.log('');
135
136
  console.log(chalk.bold('Subscription Status:'));
136
137
  console.log('');
137
- // Try to extract subscription info from various possible response shapes
138
138
  const sub = user.subscription;
139
139
  const userPlan = user.plan;
140
140
  if (sub && Object.keys(sub).length > 0) {
141
141
  const planName = sub.planName ?? sub.plan ?? sub.name ?? '—';
142
- const status = sub.status ?? '—';
143
- const interval = sub.interval ?? '—';
142
+ const planId = sub.planId ?? sub.id;
143
+ // Cross-reference with plans list for interval and credits
144
+ const matchedPlan = planId != null
145
+ ? plans.find((p) => p.id === Number(planId) || p._id === String(planId))
146
+ : plans.find((p) => p.name === planName);
147
+ const status = sub.status;
148
+ const interval = sub.interval ?? matchedPlan?.interval;
144
149
  const cancelAt = sub.cancelAtPeriodEnd;
145
150
  const periodEnd = sub.currentPeriodEnd;
146
151
  console.log(` ${chalk.dim('Plan'.padEnd(16))} : ${chalk.bold(String(planName))}`);
147
- console.log(` ${chalk.dim('Status'.padEnd(16))} : ${status === 'active' ? chalk.green('Active') : chalk.yellow(String(status))}`);
148
- console.log(` ${chalk.dim('Billing'.padEnd(16))} : ${String(interval)}`);
152
+ if (status) {
153
+ console.log(` ${chalk.dim('Status'.padEnd(16))} : ${status === 'active' ? chalk.green('Active') : chalk.yellow(status)}`);
154
+ }
155
+ else {
156
+ console.log(` ${chalk.dim('Status'.padEnd(16))} : ${chalk.green('Active')}`);
157
+ }
158
+ if (interval) {
159
+ const label = interval === 'month' ? 'Monthly' : interval === 'year' ? 'Yearly' : String(interval);
160
+ console.log(` ${chalk.dim('Billing'.padEnd(16))} : ${label}`);
161
+ }
162
+ if (matchedPlan) {
163
+ console.log(` ${chalk.dim('Price'.padEnd(16))} : ${formatPrice(matchedPlan)}`);
164
+ const credits = formatCredits(matchedPlan.rules);
165
+ if (credits !== '—') {
166
+ console.log(` ${chalk.dim('Credits'.padEnd(16))} : ${credits}`);
167
+ }
168
+ }
149
169
  if (periodEnd) {
150
170
  console.log(` ${chalk.dim('Renews On'.padEnd(16))} : ${new Date(periodEnd).toLocaleDateString()}`);
151
171
  }
@@ -157,7 +177,6 @@ const statusCmd = new Command('status')
157
177
  printKV(userPlan);
158
178
  }
159
179
  else {
160
- // No subscription info found — assume free plan
161
180
  console.log(` ${chalk.dim('Plan'.padEnd(16))} : ${chalk.bold('Free')}`);
162
181
  console.log(` ${chalk.dim('Status'.padEnd(16))} : ${chalk.green('Active')}`);
163
182
  console.log('');
@@ -1,14 +1,14 @@
1
1
  import { Command } from 'commander';
2
2
  import { input, select, confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
- import { swap, swapsSimulate } from '../api/crosschain.js';
4
+ import { swaps, swapsSimulate } from '../api/crosschain.js';
5
+ import { get } from '../api/client.js';
5
6
  import { requireAuth } from '../config.js';
6
- import { success, info, spinner, formatOrderSide, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
7
+ import { success, info, warn, spinner, formatOrderSide, assertApiOk, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel, normalizeChain } from '../utils.js';
7
8
  import { requireTouchId } from '../touchid.js';
8
9
  import { printTxResult, printKV } from '../formatters.js';
9
10
  export const swapCommand = new Command('swap')
10
11
  .description('Swap tokens (cross-chain spot trading)')
11
- .option('-c, --chain <chain>', 'Blockchain (e.g. solana, base, ethereum)')
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)')
@@ -16,9 +16,7 @@ export const swapCommand = new Command('swap')
16
16
  .option('--dry-run', 'Simulate without executing')
17
17
  .action(wrapAction(async (opts) => {
18
18
  const creds = requireAuth();
19
- // ── 1. Chain ─────────────────────────────────────────────────────────
20
- const chain = opts.chain ?? await selectChain();
21
- // ── 2. Side ──────────────────────────────────────────────────────────
19
+ // ── 1. Side ──────────────────────────────────────────────────────────
22
20
  let side = opts.side;
23
21
  if (!side) {
24
22
  side = await select({
@@ -29,21 +27,59 @@ export const swapCommand = new Command('swap')
29
27
  ],
30
28
  });
31
29
  }
32
- // ── 3. Token ───────────────────────────────────────────────────────────
30
+ // ── 2. Token ───────────────────────────────────────────────────────────
33
31
  const tokenInput = opts.token ?? await input({
34
32
  message: 'Token (contract address or ticker):',
35
33
  validate: (v) => (v.length > 0 ? true : 'Please enter a token address or ticker'),
36
34
  });
37
35
  const tokenInfo = await lookupToken(tokenInput);
36
+ // ── 3. Chain (derived from token) ────────────────────────────────────
37
+ const chain = normalizeChain(tokenInfo.chain);
38
+ if (!chain) {
39
+ warn(`Unable to determine chain for token. Raw chain value: ${tokenInfo.chain ?? 'unknown'}`);
40
+ return;
41
+ }
38
42
  // ── 4. Amount ────────────────────────────────────────────────────────
39
- const amountLabel = side === 'buy' ? 'USD amount to spend' : 'Token amount to sell';
40
- const amount = opts.amount ?? await input({
43
+ let maxBalance;
44
+ if (side === 'sell') {
45
+ const pnlRes = await get('/users/pnls/all', { token: creds.accessToken });
46
+ if (pnlRes.success && Array.isArray(pnlRes.data)) {
47
+ const match = pnlRes.data.find((t) => {
48
+ const addr = String(t.tokenAddress ?? '').toLowerCase();
49
+ const cid = String(t.chainId ?? '').toLowerCase();
50
+ const targetChain = chain.toLowerCase();
51
+ return addr === tokenInfo.address.toLowerCase() && cid === targetChain;
52
+ });
53
+ if (match) {
54
+ maxBalance = Number(match.balance ?? 0);
55
+ info(`Available balance: ${chalk.bold(String(maxBalance))} ${tokenInfo.symbol ?? ''}`);
56
+ }
57
+ }
58
+ }
59
+ const amountLabel = side === 'buy' ? 'USD amount to spend' : `Token amount to sell${maxBalance ? ' ("all" for max)' : ''}`;
60
+ let amount = opts.amount ?? await input({
41
61
  message: `${amountLabel}:`,
42
62
  validate: (v) => {
63
+ if (side === 'sell' && v.toLowerCase() === 'all')
64
+ return true;
43
65
  const n = parseFloat(v);
44
66
  return (isNaN(n) || n <= 0) ? 'Enter a valid positive number' : true;
45
67
  },
46
68
  });
69
+ if (side === 'sell') {
70
+ if (amount.toLowerCase() === 'all') {
71
+ if (!maxBalance || maxBalance <= 0) {
72
+ warn('Could not determine balance. Please enter an amount manually.');
73
+ return;
74
+ }
75
+ amount = String(maxBalance);
76
+ info(`Selling all: ${chalk.bold(amount)} ${tokenInfo.symbol ?? ''}`);
77
+ }
78
+ else if (maxBalance && parseFloat(amount) > maxBalance) {
79
+ info(`Amount exceeds balance (${maxBalance}), using max balance`);
80
+ amount = String(maxBalance);
81
+ }
82
+ }
47
83
  // ── 5. Summary ───────────────────────────────────────────────────────
48
84
  console.log('');
49
85
  console.log(chalk.bold('Swap Summary:'));
@@ -87,15 +123,21 @@ export const swapCommand = new Command('swap')
87
123
  }
88
124
  }
89
125
  // ── 8. Transaction confirmation & Touch ID ────────────────────────────
90
- await requireTransactionConfirmation(`${side.toUpperCase()} swap · ${amount} ${side === 'buy' ? 'USD' : 'tokens'} · ${chain}`, tokenInfo);
126
+ await requireTransactionConfirmation(`${side.toUpperCase()} swap · ${amount} ${side === 'buy' ? 'USD' : 'tokens'} · ${chain}`, tokenInfo, { chain, side, amount: `${amount} ${side === 'buy' ? 'USD' : '(token)'}` });
91
127
  await requireTouchId();
92
128
  // ── 9. Execute ───────────────────────────────────────────────────────
93
129
  const spin = spinner('Executing swap…');
94
- const res = await swap(creds.accessToken, {
95
- chain, side, tokenAddress: tokenInfo.address, buyUsdAmountOrSellTokenAmount: amount,
96
- });
130
+ const res = await swaps(creds.accessToken, [{
131
+ chain, side, tokenAddress: tokenInfo.address, buyUsdAmountOrSellTokenAmount: amount,
132
+ }]);
97
133
  spin.stop();
98
134
  assertApiOk(res, 'Swap failed');
99
135
  success('Swap submitted!');
100
- printTxResult(res.data);
136
+ if (Array.isArray(res.data)) {
137
+ for (const tx of res.data)
138
+ printTxResult(tx);
139
+ }
140
+ else {
141
+ printTxResult(res.data);
142
+ }
101
143
  }));
@@ -54,7 +54,7 @@ export const transferCommand = new Command('transfer')
54
54
  }
55
55
  }
56
56
  // ── 6. Transaction confirmation & Touch ID ────────────────────────────
57
- await requireTransactionConfirmation(`Transfer ${amount} tokens → ${recipient} · ${chain}`, tokenInfo);
57
+ await requireTransactionConfirmation(`Transfer ${amount} tokens → ${recipient} · ${chain}`, tokenInfo, { chain, amount });
58
58
  await requireTouchId();
59
59
  // ── 7. Execute ───────────────────────────────────────────────────────
60
60
  const spin = spinner('Processing transfer…');
@@ -82,7 +82,7 @@ export const withdrawCommand = new Command('withdraw')
82
82
  }
83
83
  }
84
84
  // ── 7. Transaction confirmation & Touch ID ────────────────────────────
85
- await requireTransactionConfirmation(`Withdraw ${amount} tokens → ${recipient} · ${chain}`, tokenInfo);
85
+ await requireTransactionConfirmation(`Withdraw ${amount} tokens → ${recipient} · ${chain}`, tokenInfo, { chain, amount });
86
86
  await requireTouchId();
87
87
  // ── 8. Execute ───────────────────────────────────────────────────────
88
88
  const spin = spinner('Processing withdrawal…');
@@ -37,13 +37,11 @@ export declare function printTable(data: Record<string, unknown>[] | object[], c
37
37
  */
38
38
  export declare function printTxResult(data: unknown): void;
39
39
  /** Spot wallet assets (WalletAsset[]) */
40
- export declare const ASSET_COLUMNS: ColumnDef[];
41
- /** Perps positions (PerpsPosition[]) */
40
+ export declare const SPOT_COLUMNS: ColumnDef[];
41
+ /** Perps positions API uses snake_case field names */
42
42
  export declare const POSITION_COLUMNS: ColumnDef[];
43
43
  /** Limit orders (LimitOrderInfo[]) */
44
44
  export declare const LIMIT_ORDER_COLUMNS: ColumnDef[];
45
- /** Copy trades (CopyTradeInfo[]) */
46
- export declare const COPY_TRADE_COLUMNS: ColumnDef[];
47
45
  /** Trending / search tokens (TokenInfo[]) */
48
46
  export declare const TOKEN_COLUMNS: ColumnDef[];
49
47
  /**
@@ -55,6 +55,9 @@ export function formatValue(value, key) {
55
55
  return value.toLocaleString('en-US', { maximumFractionDigits: 6 });
56
56
  }
57
57
  if (typeof value === 'string') {
58
+ // Hex addresses — must check before numeric coercion (0x… is valid Number)
59
+ if (/^0x[0-9a-fA-F]{20,}$/.test(value))
60
+ return chalk.yellow(value);
58
61
  // Numeric string that looks like a price / amount
59
62
  const num = Number(value);
60
63
  if (!isNaN(num) && value.trim() !== '') {
@@ -64,9 +67,6 @@ export function formatValue(value, key) {
64
67
  if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
65
68
  return new Date(value).toLocaleString();
66
69
  }
67
- // Hex addresses
68
- if (/^0x[0-9a-fA-F]{20,}$/.test(value))
69
- return chalk.yellow(value);
70
70
  // URLs
71
71
  if (value.startsWith('http'))
72
72
  return chalk.cyan.underline(value);
@@ -171,10 +171,14 @@ export function printTable(data, columns) {
171
171
  console.log(JSON.stringify(data, null, 2));
172
172
  return;
173
173
  }
174
- if (!data || data.length === 0) {
174
+ if (!data || (Array.isArray(data) && data.length === 0)) {
175
175
  console.log(chalk.dim(' No data.'));
176
176
  return;
177
177
  }
178
+ if (!Array.isArray(data)) {
179
+ printKV(data);
180
+ return;
181
+ }
178
182
  const rows = data;
179
183
  const cols = columns ?? autoColumns(rows);
180
184
  if (cols.length === 0) {
@@ -228,14 +232,27 @@ export function printTxResult(data) {
228
232
  // Pre-built column configs for known data types
229
233
  // ═══════════════════════════════════════════════════════════════════════════
230
234
  /** Spot wallet assets (WalletAsset[]) */
231
- export const ASSET_COLUMNS = [
232
- { key: 'symbol', label: 'Symbol', format: (v, row) => chalk.bold(String(v ?? row.tokenSymbol ?? '—')) },
233
- { key: 'balance', label: 'Balance', format: (v, row) => String(v ?? row.amount ?? '—') },
234
- { key: 'chain', label: 'Chain', format: (v, row) => chalk.cyan(String(v ?? row.chainName ?? '—')) },
235
- { key: 'usdValue', label: 'USD Value', format: (v) => v ? `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : chalk.dim('—') },
236
- { key: 'tokenAddress', label: 'Token Address', format: (v) => v ? chalk.dim(truncate(String(v), 16)) : chalk.dim('—') },
235
+ export const SPOT_COLUMNS = [
236
+ { key: 'tokenSymbol', label: 'Token', format: (v) => chalk.bold(String(v ?? '—')) },
237
+ { key: 'chainId', label: 'Chain', format: (v) => {
238
+ const s = String(v ?? '—');
239
+ return chalk.cyan(s.charAt(0).toUpperCase() + s.slice(1));
240
+ } },
241
+ { key: 'balance', label: 'Balance' },
242
+ { key: 'marketPrice', label: 'Price', format: (v) => v ? `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 6 })}` : chalk.dim('—') },
243
+ { key: '_value', label: 'Value', format: (v) => {
244
+ const n = Number(v ?? 0);
245
+ return n > 0 ? `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : chalk.dim('—');
246
+ } },
247
+ { key: 'unrealizedPnl', label: 'PnL', format: (v) => {
248
+ const n = Number(v ?? 0);
249
+ if (n === 0)
250
+ return chalk.dim('—');
251
+ const color = n >= 0 ? chalk.green : chalk.red;
252
+ return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
253
+ } },
237
254
  ];
238
- /** Perps positions (PerpsPosition[]) */
255
+ /** Perps positions API uses snake_case field names */
239
256
  export const POSITION_COLUMNS = [
240
257
  { key: 'symbol', label: 'Symbol', format: (v) => chalk.bold(String(v ?? '—')) },
241
258
  { key: 'side', label: 'Side', format: (v) => {
@@ -244,8 +261,8 @@ export const POSITION_COLUMNS = [
244
261
  } },
245
262
  { key: 'size', label: 'Size' },
246
263
  { key: 'entryPrice', label: 'Entry', format: (v) => formatValue(v, 'price') },
247
- { key: 'markPrice', label: 'Mark', format: (v) => formatValue(v, 'price') },
248
- { key: 'pnl', label: 'PnL', format: (v) => {
264
+ { key: 'positionValue', label: 'Value', format: (v) => formatValue(v, 'price') },
265
+ { key: 'unrealizedPnl', label: 'PnL', format: (v) => {
249
266
  if (!v && v !== 0)
250
267
  return chalk.dim('—');
251
268
  const n = Number(v);
@@ -253,6 +270,7 @@ export const POSITION_COLUMNS = [
253
270
  return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
254
271
  } },
255
272
  { key: 'leverage', label: 'Lev', format: (v) => v ? `${v}x` : chalk.dim('—') },
273
+ { key: 'marginUsed', label: 'Margin', format: (v) => formatValue(v, 'price') },
256
274
  ];
257
275
  /** Limit orders (LimitOrderInfo[]) */
258
276
  export const LIMIT_ORDER_COLUMNS = [
@@ -267,16 +285,6 @@ export const LIMIT_ORDER_COLUMNS = [
267
285
  { key: 'amount', label: 'Amount', format: (v) => v ? `$${v}` : chalk.dim('—') },
268
286
  { key: 'status', label: 'Status', format: (v) => formatValue(v, 'status') },
269
287
  ];
270
- /** Copy trades (CopyTradeInfo[]) */
271
- export const COPY_TRADE_COLUMNS = [
272
- { key: 'id', label: 'ID', format: (v) => chalk.dim(truncate(String(v ?? ''), 12)) },
273
- { key: 'name', label: 'Name', format: (v) => String(v ?? chalk.dim('—')) },
274
- { key: 'chain', label: 'Chain', format: (v) => chalk.cyan(String(v ?? '—')) },
275
- { key: 'targetAddress', label: 'Target', format: (v) => v ? chalk.yellow(truncate(String(v), 14)) : chalk.dim('—') },
276
- { key: 'fixedAmount', label: 'Amount', format: (v) => v ? `$${v}` : chalk.dim('—') },
277
- { key: 'copySell', label: 'Copy Sell', format: (v) => v ? chalk.green('Yes') : chalk.dim('No') },
278
- { key: 'status', label: 'Status', format: (v) => formatValue(v, 'status') },
279
- ];
280
288
  /** Format large numbers as $1.23B / $456.78M / $12.3K */
281
289
  function compactUsd(v) {
282
290
  if (!v && v !== 0)
package/dist/index.js CHANGED
@@ -8,13 +8,13 @@ const { version } = require('../package.json');
8
8
  import { logoutCommand } from './commands/logout.js';
9
9
  import { accountCommand } from './commands/account.js';
10
10
  import { assetsCommand } from './commands/assets.js';
11
+ import { balanceCommand } from './commands/balance.js';
11
12
  import { depositCommand } from './commands/deposit.js';
12
13
  import { withdrawCommand } from './commands/withdraw.js';
13
14
  import { swapCommand } from './commands/swap.js';
14
15
  import { transferCommand } from './commands/transfer.js';
15
16
  import { perpsCommand } from './commands/perps.js';
16
17
  import { limitOrderCommand } from './commands/limit-order.js';
17
- import { copyTradeCommand } from './commands/copy-trade.js';
18
18
  import { chatCommand } from './commands/chat.js';
19
19
  import { discoverCommand } from './commands/discover.js';
20
20
  import { configCommand } from './commands/config.js';
@@ -27,7 +27,7 @@ program
27
27
  .option('--json', 'Output raw JSON instead of formatted tables')
28
28
  .description(chalk.bold('Minara CLI') +
29
29
  ' — Your AI-powered digital finance assistant in the terminal.\n\n' +
30
- ' Login, swap, trade perps, copy-trade, and chat with Minara AI.')
30
+ ' Login, swap, trade perps, and chat with Minara AI.')
31
31
  .hook('preAction', (thisCommand) => {
32
32
  const opts = thisCommand.optsWithGlobals();
33
33
  if (opts.json)
@@ -38,6 +38,7 @@ program.addCommand(loginCommand);
38
38
  program.addCommand(logoutCommand);
39
39
  program.addCommand(accountCommand);
40
40
  // ── Wallet & Funds ───────────────────────────────────────────────────────
41
+ program.addCommand(balanceCommand);
41
42
  program.addCommand(assetsCommand);
42
43
  program.addCommand(depositCommand);
43
44
  program.addCommand(withdrawCommand);
@@ -48,7 +49,6 @@ program.addCommand(transferCommand);
48
49
  program.addCommand(perpsCommand);
49
50
  // ── Bots ─────────────────────────────────────────────────────────────────
50
51
  program.addCommand(limitOrderCommand);
51
- program.addCommand(copyTradeCommand);
52
52
  // ── AI Chat ──────────────────────────────────────────────────────────────
53
53
  program.addCommand(chatCommand);
54
54
  // ── Market ───────────────────────────────────────────────────────────────
package/dist/types.d.ts CHANGED
@@ -256,41 +256,6 @@ export interface UpdateLimitOrderDto {
256
256
  targetPrice?: number;
257
257
  expiredAt?: number;
258
258
  }
259
- export interface CreateCopyTradeDto {
260
- targetAddress: string;
261
- chain: string;
262
- name?: string;
263
- mode?: 'fixedAmount';
264
- copySell?: boolean;
265
- copySellSamePercentage?: boolean;
266
- copySellQuitPercentage?: number;
267
- fixedAmount?: number;
268
- status?: 'running' | 'paused';
269
- expiredAt?: number;
270
- }
271
- export interface CopyTradeInfo {
272
- id: string;
273
- name?: string;
274
- targetAddress: string;
275
- chain: string;
276
- mode?: string;
277
- fixedAmount?: number;
278
- copySell?: boolean;
279
- status?: string;
280
- createdAt?: string;
281
- }
282
- export interface UpdateCopyTradeDto {
283
- chain: string;
284
- name?: string;
285
- mode?: 'fixedAmount';
286
- copySell?: boolean;
287
- copySellSamePercentage?: boolean;
288
- copySellQuitPercentage?: number;
289
- fixedAmount?: number;
290
- targetAddress?: string;
291
- status?: 'running' | 'paused';
292
- expiredAt?: number;
293
- }
294
259
  export interface UserTradeConfig {
295
260
  slippage?: string;
296
261
  priorityFee?: string;
package/dist/utils.d.ts CHANGED
@@ -34,7 +34,13 @@ export interface TokenDisplayInfo {
34
34
  symbol?: string;
35
35
  name?: string;
36
36
  address: string;
37
+ chain?: string;
37
38
  }
39
+ /**
40
+ * Normalize a chain identifier from the token search API to a supported
41
+ * `Chain` value used by the swap / transfer APIs.
42
+ */
43
+ export declare function normalizeChain(raw?: string): Chain | undefined;
38
44
  /**
39
45
  * Look up token metadata by address, ticker, or name.
40
46
  *
@@ -63,6 +69,10 @@ export declare function formatTokenLabel(token: TokenDisplayInfo): string;
63
69
  *
64
70
  * Exits the process if the user declines.
65
71
  */
66
- export declare function requireTransactionConfirmation(description: string, token?: TokenDisplayInfo): Promise<void>;
72
+ export declare function requireTransactionConfirmation(description: string, token?: TokenDisplayInfo, details?: {
73
+ chain?: string;
74
+ side?: string;
75
+ amount?: string;
76
+ }): Promise<void>;
67
77
  /** Open a URL in the user's default browser (cross-platform). */
68
78
  export declare function openBrowser(url: string): void;
package/dist/utils.js CHANGED
@@ -115,6 +115,83 @@ export function wrapAction(fn) {
115
115
  }
116
116
  };
117
117
  }
118
+ const NATIVE_TOKEN_ADDRESS = {
119
+ sol: 'So11111111111111111111111111111111111111112',
120
+ solana: 'So11111111111111111111111111111111111111112',
121
+ };
122
+ const EVM_NATIVE = '0x' + '0'.repeat(40);
123
+ function resolveNativeAddress(chain) {
124
+ if (!chain)
125
+ return EVM_NATIVE;
126
+ return NATIVE_TOKEN_ADDRESS[chain.toLowerCase()] ?? EVM_NATIVE;
127
+ }
128
+ const CHAIN_ALIAS = {
129
+ sol: 'solana',
130
+ eth: 'ethereum',
131
+ arb: 'arbitrum',
132
+ op: 'optimism',
133
+ matic: 'polygon',
134
+ poly: 'polygon',
135
+ avax: 'avalanche',
136
+ bnb: 'bsc',
137
+ bera: 'berachain',
138
+ // Numeric chain IDs returned by the token search API
139
+ '101': 'solana',
140
+ '1': 'ethereum',
141
+ '8453': 'base',
142
+ '42161': 'arbitrum',
143
+ '10': 'optimism',
144
+ '56': 'bsc',
145
+ '137': 'polygon',
146
+ '43114': 'avalanche',
147
+ '81457': 'blast',
148
+ '169': 'manta',
149
+ '34443': 'mode',
150
+ '146': 'sonic',
151
+ '80094': 'berachain',
152
+ '196': 'xlayer',
153
+ '4200': 'merlin',
154
+ };
155
+ /**
156
+ * Normalize a chain identifier from the token search API to a supported
157
+ * `Chain` value used by the swap / transfer APIs.
158
+ */
159
+ export function normalizeChain(raw) {
160
+ if (!raw)
161
+ return undefined;
162
+ const lower = raw.toLowerCase();
163
+ if (SUPPORTED_CHAINS.includes(lower))
164
+ return lower;
165
+ return CHAIN_ALIAS[lower];
166
+ }
167
+ /** Capitalize chain name for display (e.g. "solana" → "Solana", "bsc" → "BSC"). */
168
+ function displayChain(raw) {
169
+ const name = normalizeChain(raw) ?? raw ?? 'unknown';
170
+ if (name === 'bsc')
171
+ return 'BSC';
172
+ return name.charAt(0).toUpperCase() + name.slice(1);
173
+ }
174
+ /** Lower = cheaper gas. Used to sort chain choices so the cheapest is first. */
175
+ const CHAIN_GAS_RANK = {
176
+ sol: 1, solana: 1, '101': 1,
177
+ base: 2, '8453': 2,
178
+ arbitrum: 3, arb: 3, '42161': 3,
179
+ optimism: 4, op: 4, '10': 4,
180
+ bsc: 5, bnb: 5, '56': 5,
181
+ polygon: 6, matic: 6, poly: 6, '137': 6,
182
+ sonic: 7, '146': 7,
183
+ avalanche: 8, avax: 8, '43114': 8,
184
+ berachain: 9, bera: 9, '80094': 9,
185
+ blast: 10, '81457': 10,
186
+ manta: 11, '169': 11,
187
+ mode: 12, '34443': 12,
188
+ ethereum: 50, eth: 50, '1': 50,
189
+ };
190
+ function chainGasRank(chain) {
191
+ if (!chain)
192
+ return 99;
193
+ return CHAIN_GAS_RANK[chain.toLowerCase()] ?? 30;
194
+ }
118
195
  /**
119
196
  * Look up token metadata by address, ticker, or name.
120
197
  *
@@ -149,25 +226,54 @@ export async function lookupToken(tokenInput) {
149
226
  if (!isTicker) {
150
227
  const exact = tokens.find((t) => t.address?.toLowerCase() === keyword.toLowerCase());
151
228
  if (exact) {
152
- return { symbol: exact.symbol, name: exact.name, address: exact.address ?? tokenInput };
229
+ return { symbol: exact.symbol, name: exact.name ?? displayChain(exact.chain), address: exact.address ?? resolveNativeAddress(exact.chain), chain: exact.chain };
153
230
  }
154
231
  }
155
232
  if (tokens.length === 1) {
156
233
  const t = tokens[0];
157
- return { symbol: t.symbol, name: t.name, address: t.address ?? tokenInput };
234
+ return { symbol: t.symbol, name: t.name ?? displayChain(t.chain), address: t.address ?? resolveNativeAddress(t.chain), chain: t.chain };
235
+ }
236
+ // Check if all results share the same symbol → multi-chain scenario
237
+ const uniqueSymbols = new Set(tokens.map((t) => t.symbol?.toLowerCase()));
238
+ if (uniqueSymbols.size === 1) {
239
+ const sorted = [...tokens].sort((a, b) => chainGasRank(a.chain) - chainGasRank(b.chain));
240
+ info(`$${sorted[0].symbol} is available on ${sorted.length} chains`);
241
+ const selected = await select({
242
+ message: 'Select chain:',
243
+ choices: sorted.map((t, i) => {
244
+ const chainName = displayChain(t.chain);
245
+ const addr = t.address
246
+ ? chalk.dim(` · ${t.address.slice(0, 10)}…${t.address.slice(-6)}`)
247
+ : chalk.dim(' · native token');
248
+ const tag = i === 0 ? chalk.green(' (lowest gas)') : '';
249
+ return { name: `${chalk.cyan(chainName)}${tag}${addr}`, value: t };
250
+ }),
251
+ });
252
+ return {
253
+ symbol: selected.symbol,
254
+ name: selected.name ?? displayChain(selected.chain),
255
+ address: selected.address ?? resolveNativeAddress(selected.chain),
256
+ chain: selected.chain,
257
+ };
158
258
  }
159
259
  info(`Found ${tokens.length} tokens matching "${tokenInput}"`);
160
260
  const selected = await select({
161
261
  message: 'Select the correct token:',
162
- choices: tokens.map((t) => ({
163
- name: `${t.symbol ? chalk.bold('$' + t.symbol) : '?'} — ${t.name ?? 'Unknown'}\n ${chalk.yellow(t.address ?? '')}`,
164
- value: t,
165
- })),
262
+ choices: tokens.map((t) => {
263
+ const sym = t.symbol ? chalk.bold('$' + t.symbol) : '?';
264
+ const chainName = displayChain(t.chain);
265
+ const label = t.name || chainName;
266
+ const desc = label ? ` — ${label}` : '';
267
+ const chainTag = chainName && chainName !== label ? chalk.dim(` [${chainName}]`) : '';
268
+ const addr = t.address ? `\n ${chalk.yellow(t.address)}` : chalk.dim('\n (native token)');
269
+ return { name: `${sym}${desc}${chainTag}${addr}`, value: t };
270
+ }),
166
271
  });
167
272
  return {
168
273
  symbol: selected.symbol,
169
- name: selected.name,
170
- address: selected.address ?? tokenInput,
274
+ name: selected.name ?? displayChain(selected.chain),
275
+ address: selected.address ?? resolveNativeAddress(selected.chain),
276
+ chain: selected.chain,
171
277
  };
172
278
  }
173
279
  catch {
@@ -197,18 +303,29 @@ export function formatTokenLabel(token) {
197
303
  *
198
304
  * Exits the process if the user declines.
199
305
  */
200
- export async function requireTransactionConfirmation(description, token) {
306
+ export async function requireTransactionConfirmation(description, token, details) {
201
307
  const config = loadConfig();
202
308
  if (config.confirmBeforeTransaction === false)
203
309
  return;
204
310
  console.log('');
205
311
  console.log(chalk.yellow('⚠'), chalk.bold('Transaction confirmation'));
312
+ if (details?.chain) {
313
+ console.log(chalk.dim(' Chain : ') + chalk.cyan(details.chain));
314
+ }
206
315
  if (token) {
207
316
  const ticker = token.symbol ? '$' + token.symbol : undefined;
208
317
  const label = [ticker, token.name].filter(Boolean).join(' — ');
209
318
  console.log(chalk.dim(' Token : ') + (label ? chalk.bold(label) : chalk.dim('Unknown token')));
210
319
  console.log(chalk.dim(' Address : ') + chalk.yellow(token.address));
211
320
  }
321
+ if (details?.side) {
322
+ const s = details.side.toLowerCase();
323
+ const colored = s === 'buy' ? chalk.green.bold(details.side.toUpperCase()) : chalk.red.bold(details.side.toUpperCase());
324
+ console.log(chalk.dim(' Side : ') + colored);
325
+ }
326
+ if (details?.amount) {
327
+ console.log(chalk.dim(' Amount : ') + chalk.bold(details.amount));
328
+ }
212
329
  console.log(chalk.dim(` Action : ${description}`));
213
330
  console.log('');
214
331
  const ok = await confirm({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",