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 +33 -41
- package/dist/api/perps.d.ts +2 -2
- package/dist/api/perps.js +4 -4
- package/dist/commands/assets.js +77 -54
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +43 -0
- package/dist/commands/deposit.js +134 -63
- package/dist/commands/limit-order.js +1 -1
- package/dist/commands/premium.js +25 -6
- package/dist/commands/swap.js +56 -14
- package/dist/commands/transfer.js +1 -1
- package/dist/commands/withdraw.js +1 -1
- package/dist/formatters.d.ts +2 -4
- package/dist/formatters.js +31 -23
- package/dist/index.js +3 -3
- package/dist/types.d.ts +0 -35
- package/dist/utils.d.ts +11 -1
- package/dist/utils.js +126 -9
- package/package.json +1 -1
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** —
|
|
23
|
-
- **Trading** — Cross-chain swaps, perpetual futures, limit orders
|
|
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 (
|
|
59
|
-
minara swap -
|
|
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
|
|
87
|
-
| `minara assets
|
|
88
|
-
| `minara assets
|
|
89
|
-
| `minara
|
|
90
|
-
| `minara
|
|
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
|
|
94
|
-
minara assets
|
|
95
|
-
minara assets
|
|
96
|
-
minara
|
|
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 (
|
|
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 -
|
|
111
|
-
minara swap -
|
|
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
|
-
> **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/dist/api/perps.d.ts
CHANGED
|
@@ -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 });
|
package/dist/commands/assets.js
CHANGED
|
@@ -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,
|
|
8
|
-
import {
|
|
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
|
|
18
|
+
const res = await get('/users/pnls/all', { token });
|
|
19
19
|
spin.stop();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
console.log(chalk.
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
perpsApi.getAccountState(token),
|
|
47
|
-
perpsApi.getPositions(token),
|
|
48
|
-
]);
|
|
83
|
+
const res = await perpsApi.getAccountSummary(token);
|
|
49
84
|
spin.stop();
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
65
|
-
if (
|
|
66
|
-
console.log(chalk.dim('
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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,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
|
+
}));
|
package/dist/commands/deposit.js
CHANGED
|
@@ -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,
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
32
|
-
getCurrentUser(creds.accessToken),
|
|
33
|
-
getAccount(creds.accessToken),
|
|
34
|
-
]);
|
|
21
|
+
const res = await getAccount(token);
|
|
35
22
|
spin.stop();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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, {
|
package/dist/commands/premium.js
CHANGED
|
@@ -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
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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('');
|
package/dist/commands/swap.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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.
|
|
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
|
-
// ──
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
95
|
-
|
|
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
|
-
|
|
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…');
|
package/dist/formatters.d.ts
CHANGED
|
@@ -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
|
|
41
|
-
/** Perps positions
|
|
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
|
/**
|
package/dist/formatters.js
CHANGED
|
@@ -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
|
|
232
|
-
{ key: '
|
|
233
|
-
{ key: '
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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: '
|
|
248
|
-
{ key: '
|
|
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,
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
164
|
-
|
|
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 ??
|
|
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({
|