minara 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/copytrade.d.ts +19 -0
- package/dist/api/copytrade.js +37 -0
- package/dist/commands/config.js +4 -1
- package/dist/commands/copy-trade.d.ts +2 -0
- package/dist/commands/copy-trade.js +170 -0
- package/dist/commands/deposit.js +1 -1
- package/dist/commands/perps.js +112 -12
- package/dist/commands/transfer.js +2 -2
- package/dist/commands/withdraw.js +2 -2
- package/dist/config.js +2 -3
- package/dist/oauth-server.js +10 -1
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +44 -6
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CreateCopyTradeDto, UpdateCopyTradeDto, CopyTradeInfo } from '../types.js';
|
|
2
|
+
/** Create copy trade */
|
|
3
|
+
export declare function createCopyTrade(token: string, dto: CreateCopyTradeDto): Promise<import("../types.js").ApiResponse<CopyTradeInfo>>;
|
|
4
|
+
/** List user's copy trades */
|
|
5
|
+
export declare function listCopyTrades(token: string): Promise<import("../types.js").ApiResponse<CopyTradeInfo[]>>;
|
|
6
|
+
/** Get copy trade by ID */
|
|
7
|
+
export declare function getCopyTrade(token: string, id: string): Promise<import("../types.js").ApiResponse<CopyTradeInfo>>;
|
|
8
|
+
/** Update copy trade */
|
|
9
|
+
export declare function updateCopyTrade(token: string, id: string, dto: UpdateCopyTradeDto): Promise<import("../types.js").ApiResponse<CopyTradeInfo>>;
|
|
10
|
+
/** Delete copy trade */
|
|
11
|
+
export declare function deleteCopyTrade(token: string, id: string): Promise<import("../types.js").ApiResponse<void>>;
|
|
12
|
+
/** Start copy trade */
|
|
13
|
+
export declare function startCopyTrade(token: string, id: string): Promise<import("../types.js").ApiResponse<void>>;
|
|
14
|
+
/** Stop copy trade */
|
|
15
|
+
export declare function stopCopyTrade(token: string, id: string): Promise<import("../types.js").ApiResponse<void>>;
|
|
16
|
+
/** Get copy trade activity */
|
|
17
|
+
export declare function getCopyTradeActivity(token: string, id: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
|
|
18
|
+
/** Get copy trade PnL chart */
|
|
19
|
+
export declare function getCopyTradePnl(token: string, id: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { get, post, patch, del } from './client.js';
|
|
2
|
+
/** Create copy trade */
|
|
3
|
+
export function createCopyTrade(token, dto) {
|
|
4
|
+
return post('/copy-trade', { token, body: dto });
|
|
5
|
+
}
|
|
6
|
+
/** List user's copy trades */
|
|
7
|
+
export function listCopyTrades(token) {
|
|
8
|
+
return get('/copy-trade', { token });
|
|
9
|
+
}
|
|
10
|
+
/** Get copy trade by ID */
|
|
11
|
+
export function getCopyTrade(token, id) {
|
|
12
|
+
return get(`/copy-trade/${encodeURIComponent(id)}`, { token });
|
|
13
|
+
}
|
|
14
|
+
/** Update copy trade */
|
|
15
|
+
export function updateCopyTrade(token, id, dto) {
|
|
16
|
+
return patch(`/copy-trade/${encodeURIComponent(id)}`, { token, body: dto });
|
|
17
|
+
}
|
|
18
|
+
/** Delete copy trade */
|
|
19
|
+
export function deleteCopyTrade(token, id) {
|
|
20
|
+
return del(`/copy-trade/${encodeURIComponent(id)}`, { token });
|
|
21
|
+
}
|
|
22
|
+
/** Start copy trade */
|
|
23
|
+
export function startCopyTrade(token, id) {
|
|
24
|
+
return patch(`/copy-trade/${encodeURIComponent(id)}/start`, { token });
|
|
25
|
+
}
|
|
26
|
+
/** Stop copy trade */
|
|
27
|
+
export function stopCopyTrade(token, id) {
|
|
28
|
+
return patch(`/copy-trade/${encodeURIComponent(id)}/stop`, { token });
|
|
29
|
+
}
|
|
30
|
+
/** Get copy trade activity */
|
|
31
|
+
export function getCopyTradeActivity(token, id) {
|
|
32
|
+
return get(`/copy-trade/${encodeURIComponent(id)}/activity`, { token });
|
|
33
|
+
}
|
|
34
|
+
/** Get copy trade PnL chart */
|
|
35
|
+
export function getCopyTradePnl(token, id) {
|
|
36
|
+
return get(`/copy-trade/${encodeURIComponent(id)}/pnl/chart`, { token });
|
|
37
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -41,7 +41,10 @@ export const configCommand = new Command('config')
|
|
|
41
41
|
default: config.baseUrl,
|
|
42
42
|
validate: (v) => {
|
|
43
43
|
try {
|
|
44
|
-
new URL(v);
|
|
44
|
+
const u = new URL(v);
|
|
45
|
+
if (u.protocol !== 'https:' && u.hostname !== 'localhost' && u.hostname !== '127.0.0.1') {
|
|
46
|
+
return 'Base URL must use HTTPS (HTTP allowed only for localhost)';
|
|
47
|
+
}
|
|
45
48
|
return true;
|
|
46
49
|
}
|
|
47
50
|
catch {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { input, select, confirm, number as numberPrompt } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as ctApi from '../api/copytrade.js';
|
|
5
|
+
import { requireAuth } from '../config.js';
|
|
6
|
+
import { success, info, spinner, assertApiOk, selectChain, wrapAction } from '../utils.js';
|
|
7
|
+
// ─── create ──────────────────────────────────────────────────────────────
|
|
8
|
+
const createCmd = new Command('create')
|
|
9
|
+
.description('Create a copy trade bot')
|
|
10
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
11
|
+
.action(wrapAction(async (opts) => {
|
|
12
|
+
const creds = requireAuth();
|
|
13
|
+
const chain = await selectChain('Chain:', true);
|
|
14
|
+
const targetAddress = await input({
|
|
15
|
+
message: 'Target wallet address to copy:',
|
|
16
|
+
validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
|
|
17
|
+
});
|
|
18
|
+
const name = await input({ message: 'Name for this copy trade (optional):' }) || undefined;
|
|
19
|
+
const fixedAmount = await numberPrompt({ message: 'Fixed buy amount (USD) per copy:', min: 1, required: true });
|
|
20
|
+
const copySell = await confirm({ message: 'Also copy sell actions?', default: true });
|
|
21
|
+
let copySellSamePercentage = false;
|
|
22
|
+
let copySellQuitPercentage;
|
|
23
|
+
if (copySell) {
|
|
24
|
+
copySellSamePercentage = await confirm({
|
|
25
|
+
message: 'Copy sell with same percentage as target?',
|
|
26
|
+
default: true,
|
|
27
|
+
});
|
|
28
|
+
copySellQuitPercentage = (await numberPrompt({
|
|
29
|
+
message: 'Clear position alert % (when target sells >= this %, clear your position; 0 to skip):',
|
|
30
|
+
default: 0,
|
|
31
|
+
})) || undefined;
|
|
32
|
+
}
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.bold('Copy Trade Bot:'));
|
|
35
|
+
console.log(` Chain : ${chalk.cyan(chain)}`);
|
|
36
|
+
console.log(` Target : ${chalk.yellow(targetAddress)}`);
|
|
37
|
+
if (name)
|
|
38
|
+
console.log(` Name : ${name}`);
|
|
39
|
+
console.log(` Buy Amount : $${fixedAmount}`);
|
|
40
|
+
console.log(` Copy Sell : ${copySell ? 'Yes' : 'No'}`);
|
|
41
|
+
if (copySellSamePercentage)
|
|
42
|
+
console.log(` Same % : Yes`);
|
|
43
|
+
if (copySellQuitPercentage)
|
|
44
|
+
console.log(` Quit Threshold : ${copySellQuitPercentage}%`);
|
|
45
|
+
console.log('');
|
|
46
|
+
if (!opts.yes) {
|
|
47
|
+
const ok = await confirm({ message: 'Create this copy trade?', default: false });
|
|
48
|
+
if (!ok)
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const spin = spinner('Creating copy trade…');
|
|
52
|
+
const res = await ctApi.createCopyTrade(creds.accessToken, {
|
|
53
|
+
chain, targetAddress, name,
|
|
54
|
+
mode: 'fixedAmount',
|
|
55
|
+
fixedAmount: fixedAmount,
|
|
56
|
+
copySell,
|
|
57
|
+
copySellSamePercentage,
|
|
58
|
+
copySellQuitPercentage,
|
|
59
|
+
});
|
|
60
|
+
spin.stop();
|
|
61
|
+
assertApiOk(res, 'Failed to create copy trade');
|
|
62
|
+
success('Copy trade created!');
|
|
63
|
+
if (res.data)
|
|
64
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
65
|
+
}));
|
|
66
|
+
// ─── list ────────────────────────────────────────────────────────────────
|
|
67
|
+
const listCmd = new Command('list')
|
|
68
|
+
.alias('ls')
|
|
69
|
+
.description('List your copy trades')
|
|
70
|
+
.action(wrapAction(async () => {
|
|
71
|
+
const creds = requireAuth();
|
|
72
|
+
const spin = spinner('Fetching copy trades…');
|
|
73
|
+
const res = await ctApi.listCopyTrades(creds.accessToken);
|
|
74
|
+
spin.stop();
|
|
75
|
+
assertApiOk(res, 'Failed to fetch copy trades');
|
|
76
|
+
const data = res.data;
|
|
77
|
+
if (!data || data.length === 0) {
|
|
78
|
+
console.log(chalk.dim('No copy trades.'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(JSON.stringify(data, null, 2));
|
|
82
|
+
}));
|
|
83
|
+
// ─── start / stop ────────────────────────────────────────────────────────
|
|
84
|
+
async function pickCopyTrade(token) {
|
|
85
|
+
const spin = spinner('Fetching copy trades…');
|
|
86
|
+
const res = await ctApi.listCopyTrades(token);
|
|
87
|
+
spin.stop();
|
|
88
|
+
const trades = res.data;
|
|
89
|
+
if (!trades || trades.length === 0) {
|
|
90
|
+
info('No copy trades found.');
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
return select({
|
|
94
|
+
message: 'Select copy trade:',
|
|
95
|
+
choices: trades.map((t) => ({
|
|
96
|
+
name: `[${t.id.slice(0, 12)}…] ${t.name ?? t.targetAddress} status=${t.status ?? '?'}`,
|
|
97
|
+
value: t.id,
|
|
98
|
+
})),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const startCmd = new Command('start')
|
|
102
|
+
.description('Start (resume) a copy trade')
|
|
103
|
+
.argument('[id]', 'Copy trade ID')
|
|
104
|
+
.action(wrapAction(async (idArg) => {
|
|
105
|
+
const creds = requireAuth();
|
|
106
|
+
const id = idArg ?? await pickCopyTrade(creds.accessToken);
|
|
107
|
+
const spin = spinner('Starting…');
|
|
108
|
+
const res = await ctApi.startCopyTrade(creds.accessToken, id);
|
|
109
|
+
spin.stop();
|
|
110
|
+
assertApiOk(res, 'Failed to start copy trade');
|
|
111
|
+
success('Copy trade started.');
|
|
112
|
+
}));
|
|
113
|
+
const stopCmd = new Command('stop')
|
|
114
|
+
.description('Stop (pause) a copy trade')
|
|
115
|
+
.argument('[id]', 'Copy trade ID')
|
|
116
|
+
.action(wrapAction(async (idArg) => {
|
|
117
|
+
const creds = requireAuth();
|
|
118
|
+
const id = idArg ?? await pickCopyTrade(creds.accessToken);
|
|
119
|
+
const ok = await confirm({ message: `Stop copy trade ${id.slice(0, 12)}…?`, default: false });
|
|
120
|
+
if (!ok)
|
|
121
|
+
return;
|
|
122
|
+
const spin = spinner('Stopping…');
|
|
123
|
+
const res = await ctApi.stopCopyTrade(creds.accessToken, id);
|
|
124
|
+
spin.stop();
|
|
125
|
+
assertApiOk(res, 'Failed to stop copy trade');
|
|
126
|
+
success('Copy trade stopped.');
|
|
127
|
+
}));
|
|
128
|
+
// ─── delete ──────────────────────────────────────────────────────────────
|
|
129
|
+
const deleteCmd = new Command('delete')
|
|
130
|
+
.description('Delete a copy trade')
|
|
131
|
+
.argument('[id]', 'Copy trade ID')
|
|
132
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
133
|
+
.action(wrapAction(async (idArg, opts) => {
|
|
134
|
+
const creds = requireAuth();
|
|
135
|
+
const id = idArg ?? await pickCopyTrade(creds.accessToken);
|
|
136
|
+
if (!opts?.yes) {
|
|
137
|
+
const ok = await confirm({ message: `Delete copy trade ${id.slice(0, 12)}…? This cannot be undone.`, default: false });
|
|
138
|
+
if (!ok)
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const spin = spinner('Deleting…');
|
|
142
|
+
const res = await ctApi.deleteCopyTrade(creds.accessToken, id);
|
|
143
|
+
spin.stop();
|
|
144
|
+
assertApiOk(res, 'Failed to delete copy trade');
|
|
145
|
+
success('Copy trade deleted.');
|
|
146
|
+
}));
|
|
147
|
+
// ─── parent ──────────────────────────────────────────────────────────────
|
|
148
|
+
export const copyTradeCommand = new Command('copy-trade')
|
|
149
|
+
.alias('ct')
|
|
150
|
+
.description('Copy trading — follow wallet addresses')
|
|
151
|
+
.addCommand(createCmd)
|
|
152
|
+
.addCommand(listCmd)
|
|
153
|
+
.addCommand(startCmd)
|
|
154
|
+
.addCommand(stopCmd)
|
|
155
|
+
.addCommand(deleteCmd)
|
|
156
|
+
.action(wrapAction(async () => {
|
|
157
|
+
const action = await select({
|
|
158
|
+
message: 'Copy Trade:',
|
|
159
|
+
choices: [
|
|
160
|
+
{ name: 'Create a new copy trade', value: 'create' },
|
|
161
|
+
{ name: 'List copy trades', value: 'list' },
|
|
162
|
+
{ name: 'Start a copy trade', value: 'start' },
|
|
163
|
+
{ name: 'Stop a copy trade', value: 'stop' },
|
|
164
|
+
{ name: 'Delete a copy trade', value: 'delete' },
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
const sub = copyTradeCommand.commands.find((c) => c.name() === action);
|
|
168
|
+
if (sub)
|
|
169
|
+
await sub.parseAsync([], { from: 'user' });
|
|
170
|
+
}));
|
package/dist/commands/deposit.js
CHANGED
|
@@ -53,7 +53,7 @@ async function showSpotDeposit(token) {
|
|
|
53
53
|
console.log('');
|
|
54
54
|
}
|
|
55
55
|
// ─── moonpay (credit card on-ramp) ───────────────────────────────────────
|
|
56
|
-
const MOONPAY_PK = 'pk_live_yIf64w79W6ufwip4j51PWbymdwGtI';
|
|
56
|
+
const MOONPAY_PK = process.env.MOONPAY_PK ?? 'pk_live_yIf64w79W6ufwip4j51PWbymdwGtI';
|
|
57
57
|
const MOONPAY_CURRENCIES = [
|
|
58
58
|
{ name: 'USDC (Base)', code: 'usdc_base', network: 'base' },
|
|
59
59
|
{ name: 'USDC (Ethereum)', code: 'usdc', network: 'ethereum' },
|
package/dist/commands/perps.js
CHANGED
|
@@ -3,7 +3,7 @@ import { input, select, confirm, number as numberPrompt } from '@inquirer/prompt
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import * as perpsApi from '../api/perps.js';
|
|
5
5
|
import { requireAuth } from '../config.js';
|
|
6
|
-
import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation } from '../utils.js';
|
|
6
|
+
import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation, validateAddress } from '../utils.js';
|
|
7
7
|
import { requireTouchId } from '../touchid.js';
|
|
8
8
|
import { printTxResult, printTable, printKV, POSITION_COLUMNS, FILL_COLUMNS } from '../formatters.js';
|
|
9
9
|
// ─── deposit ─────────────────────────────────────────────────────────────
|
|
@@ -48,7 +48,7 @@ const withdrawCmd = new Command('withdraw')
|
|
|
48
48
|
: await numberPrompt({ message: 'USDC amount to withdraw:', min: 0.01, required: true });
|
|
49
49
|
const toAddress = opts.to ?? await input({
|
|
50
50
|
message: 'Destination address:',
|
|
51
|
-
validate: (v) => (v
|
|
51
|
+
validate: (v) => validateAddress(v, 'arbitrum'),
|
|
52
52
|
});
|
|
53
53
|
console.log(`\n Withdraw : ${chalk.bold(amount)} USDC → ${chalk.yellow(toAddress)}\n`);
|
|
54
54
|
warn('Withdrawals may take time to process.');
|
|
@@ -286,6 +286,8 @@ const cancelCmd = new Command('cancel')
|
|
|
286
286
|
const closeCmd = new Command('close')
|
|
287
287
|
.description('Close an open perps position at market price')
|
|
288
288
|
.option('-y, --yes', 'Skip confirmation')
|
|
289
|
+
.option('-a, --all', 'Close all open positions (non-interactive)')
|
|
290
|
+
.option('-s, --symbol <symbol>', 'Close position by symbol (non-interactive, e.g. BTC, ETH)')
|
|
289
291
|
.action(wrapAction(async (opts) => {
|
|
290
292
|
const creds = requireAuth();
|
|
291
293
|
const spin = spinner('Fetching positions…');
|
|
@@ -307,21 +309,119 @@ const closeCmd = new Command('close')
|
|
|
307
309
|
const color = n >= 0 ? chalk.green : chalk.red;
|
|
308
310
|
return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
|
|
309
311
|
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
// Helper to close specific positions (used by --all and --symbol)
|
|
313
|
+
const closePositions = async (positionsToClose, title) => {
|
|
314
|
+
console.log('');
|
|
315
|
+
console.log(chalk.bold(title));
|
|
316
|
+
console.log(` Positions to close: ${positionsToClose.length}`);
|
|
317
|
+
positionsToClose.forEach((p) => {
|
|
313
318
|
const symbol = String(p.symbol ?? '');
|
|
314
319
|
const side = String(p.side ?? '').toLowerCase();
|
|
315
|
-
const sideLabel = side === 'long' || side === 'buy' ?
|
|
320
|
+
const sideLabel = side === 'long' || side === 'buy' ? 'LONG' : 'SHORT';
|
|
316
321
|
const sz = String(p.size ?? '');
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
+
console.log(` - ${symbol} ${sideLabel} ${sz}`);
|
|
323
|
+
});
|
|
324
|
+
console.log('');
|
|
325
|
+
if (!opts.yes) {
|
|
326
|
+
await requireTransactionConfirmation(`Close ${positionsToClose.length} position(s) @ Market`);
|
|
327
|
+
}
|
|
328
|
+
await requireTouchId();
|
|
329
|
+
const orderSpin = spinner('Closing positions…');
|
|
330
|
+
const results = [];
|
|
331
|
+
for (const pos of positionsToClose) {
|
|
332
|
+
const symbol = String(pos.symbol ?? '');
|
|
333
|
+
const side = String(pos.side ?? '').toLowerCase();
|
|
334
|
+
const sz = String(pos.size ?? '');
|
|
335
|
+
const isLong = side === 'long' || side === 'buy';
|
|
336
|
+
const isBuy = !isLong;
|
|
337
|
+
const assetMeta = assets.find((a) => a.name.toUpperCase() === symbol.toUpperCase());
|
|
338
|
+
const marketPx = assetMeta?.markPx;
|
|
339
|
+
if (!marketPx || marketPx <= 0) {
|
|
340
|
+
results.push({ symbol, side, success: false, error: 'Could not fetch price' });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const slippagePx = isBuy ? marketPx * 1.01 : marketPx * 0.99;
|
|
344
|
+
const limitPx = slippagePx.toPrecision(5);
|
|
345
|
+
const order = {
|
|
346
|
+
a: symbol,
|
|
347
|
+
b: isBuy,
|
|
348
|
+
p: limitPx,
|
|
349
|
+
s: sz,
|
|
350
|
+
r: true,
|
|
351
|
+
t: { trigger: { triggerPx: String(marketPx), tpsl: 'tp', isMarket: true } },
|
|
322
352
|
};
|
|
323
|
-
|
|
353
|
+
try {
|
|
354
|
+
const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
|
|
355
|
+
if (orderRes.success) {
|
|
356
|
+
results.push({ symbol, side, success: true });
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const errMsg = orderRes.error ? `${orderRes.error.code}: ${orderRes.error.message}` : 'Unknown error';
|
|
360
|
+
results.push({ symbol, side, success: false, error: errMsg });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
results.push({ symbol, side, success: false, error: String(e) });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
orderSpin.stop();
|
|
368
|
+
// Report results
|
|
369
|
+
const succeeded = results.filter((r) => r.success);
|
|
370
|
+
const failed = results.filter((r) => !r.success);
|
|
371
|
+
if (succeeded.length > 0) {
|
|
372
|
+
success(`Closed ${succeeded.length} position(s):`);
|
|
373
|
+
succeeded.forEach((r) => {
|
|
374
|
+
console.log(` ✓ ${r.symbol} ${r.side.toUpperCase()}`);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (failed.length > 0) {
|
|
378
|
+
warn(`Failed to close ${failed.length} position(s):`);
|
|
379
|
+
failed.forEach((r) => {
|
|
380
|
+
console.log(` ✗ ${r.symbol} ${r.side.toUpperCase()}: ${r.error}`);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
// If --all flag is set, close all positions directly (non-interactive)
|
|
385
|
+
if (opts.all) {
|
|
386
|
+
await closePositions(positions, 'Close ALL Positions:');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// If --symbol flag is set, close positions matching the symbol (non-interactive)
|
|
390
|
+
if (opts.symbol) {
|
|
391
|
+
const symbolUpper = opts.symbol.toUpperCase();
|
|
392
|
+
const matchingPositions = positions.filter((p) => String(p.symbol ?? '').toUpperCase() === symbolUpper);
|
|
393
|
+
if (matchingPositions.length === 0) {
|
|
394
|
+
warn(`No open positions found for symbol: ${opts.symbol}`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
await closePositions(matchingPositions, `Close ${opts.symbol.toUpperCase()} Positions:`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const positionChoices = positions.map((p) => {
|
|
401
|
+
const symbol = String(p.symbol ?? '');
|
|
402
|
+
const side = String(p.side ?? '').toLowerCase();
|
|
403
|
+
const sideLabel = side === 'long' || side === 'buy' ? chalk.green('LONG') : chalk.red('SHORT');
|
|
404
|
+
const sz = String(p.size ?? '');
|
|
405
|
+
const entry = fmt(Number(p.entryPrice ?? 0));
|
|
406
|
+
const pnl = pnlFmt(Number(p.unrealizedPnl ?? 0));
|
|
407
|
+
return {
|
|
408
|
+
name: `${chalk.bold(symbol.padEnd(6))} ${sideLabel} ${sz} @ ${chalk.yellow(entry)} PnL: ${pnl}`,
|
|
409
|
+
value: p,
|
|
410
|
+
};
|
|
324
411
|
});
|
|
412
|
+
// Add "ALL POSITIONS" option at the beginning
|
|
413
|
+
const allOption = { name: chalk.bold.cyan('[ CLOSE ALL POSITIONS ]'), value: '__ALL__' };
|
|
414
|
+
const choices = [allOption, ...positionChoices];
|
|
415
|
+
const selected = await select({
|
|
416
|
+
message: 'Select position to close:',
|
|
417
|
+
choices,
|
|
418
|
+
});
|
|
419
|
+
// Handle "ALL POSITIONS" selection
|
|
420
|
+
if (selected === '__ALL__') {
|
|
421
|
+
await closePositions(positions, 'Close ALL Positions:');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Single position close (existing logic)
|
|
325
425
|
const symbol = String(selected.symbol ?? '');
|
|
326
426
|
const side = String(selected.side ?? '').toLowerCase();
|
|
327
427
|
const sz = String(selected.size ?? '');
|
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { input } from '@inquirer/prompts';
|
|
3
3
|
import { transfer } from '../api/crosschain.js';
|
|
4
4
|
import { requireAuth } from '../config.js';
|
|
5
|
-
import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken } from '../utils.js';
|
|
5
|
+
import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, validateAddress } from '../utils.js';
|
|
6
6
|
import { requireTouchId } from '../touchid.js';
|
|
7
7
|
import { printTxResult } from '../formatters.js';
|
|
8
8
|
export const transferCommand = new Command('transfer')
|
|
@@ -33,7 +33,7 @@ export const transferCommand = new Command('transfer')
|
|
|
33
33
|
// ── 4. Recipient ─────────────────────────────────────────────────────
|
|
34
34
|
const recipient = opts.to ?? await input({
|
|
35
35
|
message: 'Recipient address:',
|
|
36
|
-
validate: (v) => (v
|
|
36
|
+
validate: (v) => validateAddress(v, chain),
|
|
37
37
|
});
|
|
38
38
|
// ── 5. Confirm & Touch ID ──────────────────────────────────────────
|
|
39
39
|
if (!opts.yes) {
|
|
@@ -3,7 +3,7 @@ import { input } from '@inquirer/prompts';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { transfer, getAssets } from '../api/crosschain.js';
|
|
5
5
|
import { requireAuth } from '../config.js';
|
|
6
|
-
import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken } from '../utils.js';
|
|
6
|
+
import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, validateAddress } from '../utils.js';
|
|
7
7
|
import { requireTouchId } from '../touchid.js';
|
|
8
8
|
import { printTxResult } from '../formatters.js';
|
|
9
9
|
export const withdrawCommand = new Command('withdraw')
|
|
@@ -57,7 +57,7 @@ export const withdrawCommand = new Command('withdraw')
|
|
|
57
57
|
// ── 5. Destination ───────────────────────────────────────────────────
|
|
58
58
|
const recipient = opts.to ?? await input({
|
|
59
59
|
message: 'Destination address (your external wallet):',
|
|
60
|
-
validate: (v) => (v
|
|
60
|
+
validate: (v) => validateAddress(v, chain),
|
|
61
61
|
});
|
|
62
62
|
// ── 6. Confirm & Touch ID ──────────────────────────────────────────
|
|
63
63
|
if (!opts.yes) {
|
package/dist/config.js
CHANGED
|
@@ -14,8 +14,7 @@ function ensureDir() {
|
|
|
14
14
|
// ─── Credentials ─────────────────────────────────────────────────────────────
|
|
15
15
|
export function saveCredentials(creds) {
|
|
16
16
|
ensureDir();
|
|
17
|
-
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), 'utf-8');
|
|
18
|
-
chmodSync(CREDENTIALS_FILE, 0o600);
|
|
17
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
19
18
|
}
|
|
20
19
|
export function loadCredentials() {
|
|
21
20
|
if (!existsSync(CREDENTIALS_FILE))
|
|
@@ -65,7 +64,7 @@ export function saveConfig(config) {
|
|
|
65
64
|
ensureDir();
|
|
66
65
|
const current = loadConfig();
|
|
67
66
|
const merged = { ...current, ...config };
|
|
68
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
|
|
67
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
69
68
|
}
|
|
70
69
|
export function getMinaraDir() {
|
|
71
70
|
ensureDir();
|
package/dist/oauth-server.js
CHANGED
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createServer } from 'node:http';
|
|
13
13
|
import { URL } from 'node:url';
|
|
14
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
15
|
+
function escapeHtml(str) {
|
|
16
|
+
return str
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, ''');
|
|
22
|
+
}
|
|
14
23
|
// ── HTML responses ────────────────────────────────────────────────────────
|
|
15
24
|
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
16
25
|
<html>
|
|
@@ -48,7 +57,7 @@ const ERROR_HTML = (msg) => `<!DOCTYPE html>
|
|
|
48
57
|
<body>
|
|
49
58
|
<div class="card">
|
|
50
59
|
<h1>✖ Login Failed</h1>
|
|
51
|
-
<p>${msg}</p>
|
|
60
|
+
<p>${escapeHtml(msg)}</p>
|
|
52
61
|
<p>Please return to the terminal and try again.</p>
|
|
53
62
|
</div>
|
|
54
63
|
</body>
|
package/dist/utils.d.ts
CHANGED
|
@@ -75,5 +75,10 @@ export declare function requireTransactionConfirmation(description: string, toke
|
|
|
75
75
|
amount?: string;
|
|
76
76
|
destination?: string;
|
|
77
77
|
}): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Validate a blockchain address based on the target chain.
|
|
80
|
+
* Returns `true` on success or an error message string on failure.
|
|
81
|
+
*/
|
|
82
|
+
export declare function validateAddress(address: string, chain?: string): true | string;
|
|
78
83
|
/** Open a URL in the user's default browser (cross-platform). */
|
|
79
84
|
export declare function openBrowser(url: string): void;
|
package/dist/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
4
|
import { platform } from 'node:os';
|
|
5
5
|
import { select, confirm } from '@inquirer/prompts';
|
|
6
6
|
import { SUPPORTED_CHAINS } from './types.js';
|
|
@@ -340,16 +340,54 @@ export async function requireTransactionConfirmation(description, token, details
|
|
|
340
340
|
process.exit(0);
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
|
+
// ─── Address validation ──────────────────────────────────────────────────────
|
|
344
|
+
const SOLANA_ADDR_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
345
|
+
const EVM_ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
346
|
+
/**
|
|
347
|
+
* Validate a blockchain address based on the target chain.
|
|
348
|
+
* Returns `true` on success or an error message string on failure.
|
|
349
|
+
*/
|
|
350
|
+
export function validateAddress(address, chain) {
|
|
351
|
+
const v = address.trim();
|
|
352
|
+
if (!v)
|
|
353
|
+
return 'Address is required';
|
|
354
|
+
const c = chain?.toLowerCase();
|
|
355
|
+
if (c === 'solana' || c === 'sol' || c === '101') {
|
|
356
|
+
if (!SOLANA_ADDR_RE.test(v))
|
|
357
|
+
return 'Invalid Solana address (expected base58, 32–44 chars)';
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
if (c && c !== 'solana') {
|
|
361
|
+
if (!EVM_ADDR_RE.test(v))
|
|
362
|
+
return 'Invalid EVM address (expected 0x + 40 hex chars)';
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
// Unknown chain — accept either format
|
|
366
|
+
if (!SOLANA_ADDR_RE.test(v) && !EVM_ADDR_RE.test(v)) {
|
|
367
|
+
return 'Invalid address format';
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
343
371
|
// ─── Browser ──────────────────────────────────────────────────────────────────
|
|
344
372
|
/** Open a URL in the user's default browser (cross-platform). */
|
|
345
373
|
export function openBrowser(url) {
|
|
346
374
|
const plat = platform();
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
375
|
+
let cmd;
|
|
376
|
+
let args;
|
|
377
|
+
if (plat === 'darwin') {
|
|
378
|
+
cmd = 'open';
|
|
379
|
+
args = [url];
|
|
380
|
+
}
|
|
381
|
+
else if (plat === 'win32') {
|
|
382
|
+
cmd = 'cmd';
|
|
383
|
+
args = ['/c', 'start', '', url];
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
cmd = 'xdg-open';
|
|
387
|
+
args = [url];
|
|
388
|
+
}
|
|
389
|
+
execFile(cmd, args, (err) => {
|
|
351
390
|
if (err) {
|
|
352
|
-
// Don't crash — the user can manually open the URL
|
|
353
391
|
console.log(chalk.dim(`Could not open browser automatically. Please open this URL manually:`));
|
|
354
392
|
console.log(chalk.cyan(url));
|
|
355
393
|
}
|