minara 0.2.1 → 0.2.3

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
@@ -40,7 +40,7 @@ npx minara --help
40
40
  ## Quick Start
41
41
 
42
42
  ```bash
43
- # Login (interactive — choose email, Google, or Apple)
43
+ # Login (interactive — device code or email)
44
44
  minara login
45
45
 
46
46
  # Check your account
@@ -68,15 +68,14 @@ minara discover trending
68
68
 
69
69
  | Command | Description |
70
70
  | ---------------- | ------------------------------------------- |
71
- | `minara login` | Login via email, Google, or Apple ID |
71
+ | `minara login` | Login via device code or email |
72
72
  | `minara logout` | Logout and clear local credentials |
73
73
  | `minara account` | View your account info and wallet addresses |
74
74
 
75
75
  ```bash
76
- minara login # Interactive method selection
76
+ minara login # Interactive: device code (default) or email
77
+ minara login --device # Device code (opens browser to verify)
77
78
  minara login -e user@mail.com # Email verification code
78
- minara login --google # Google OAuth (opens browser)
79
- minara login --apple # Apple ID (opens browser)
80
79
  ```
81
80
 
82
81
  ### Wallet & Funds
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { select, confirm, number as numberPrompt } from '@inquirer/prompts';
2
+ import { select, number as numberPrompt } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { getAccount } from '../api/crosschain.js';
5
5
  import { getCurrentUser } from '../api/auth.js';
@@ -113,16 +113,12 @@ async function transferSpotToPerps(token, opts) {
113
113
  console.error(chalk.red('✖'), 'Minimum deposit is 5 USDC');
114
114
  process.exit(1);
115
115
  }
116
- console.log(`\n Transfer : ${chalk.bold(amount)} USDC ${chalk.dim('Spot wallet')} → ${chalk.cyan('Perps wallet')}\n`);
117
116
  if (!opts?.yes) {
118
- const ok = await confirm({ message: 'Confirm transfer from Spot to Perps?', default: true });
119
- if (!ok)
120
- return;
117
+ await requireTransactionConfirmation(`Transfer ${amount} USDC from Spot Perps`, undefined, {
118
+ amount: `${amount} USDC`,
119
+ side: 'Spot → Perps',
120
+ });
121
121
  }
122
- await requireTransactionConfirmation(`Transfer ${amount} USDC from Spot → Perps`, undefined, {
123
- amount: `${amount} USDC`,
124
- side: 'Spot → Perps',
125
- });
126
122
  await requireTouchId();
127
123
  const spin = spinner('Transferring…');
128
124
  const res = await perpsApi.deposit(token, { usdcAmount: amount });
@@ -1,11 +1,9 @@
1
1
  import { Command } from 'commander';
2
2
  import { input, select, confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
- import { sendEmailCode, verifyEmailCode, getOAuthUrl, getCurrentUser, startDeviceAuth, getDeviceAuthStatus, } from '../api/auth.js';
4
+ import { sendEmailCode, verifyEmailCode, startDeviceAuth, getDeviceAuthStatus, } from '../api/auth.js';
5
5
  import { saveCredentials, loadCredentials, loadConfig, saveConfig } from '../config.js';
6
- import { success, error, info, warn, spinner, openBrowser, unwrapApi, wrapAction } from '../utils.js';
7
- import { startOAuthServer } from '../oauth-server.js';
8
- import { OAUTH_PROVIDERS } from '../types.js';
6
+ import { success, error, info, spinner, openBrowser, unwrapApi, wrapAction } from '../utils.js';
9
7
  import { isTouchIdAvailable } from '../touchid.js';
10
8
  // ─── Email login flow ─────────────────────────────────────────────────────
11
9
  async function loginWithEmail(emailOpt) {
@@ -42,72 +40,6 @@ async function loginWithEmail(emailOpt) {
42
40
  });
43
41
  success(`Welcome${user.displayName ? `, ${user.displayName}` : ''}! Credentials saved to ~/.minara/`);
44
42
  }
45
- // ─── OAuth login flow ──────────────────────────────────────────────────────
46
- async function loginWithOAuth(provider) {
47
- const providerName = OAUTH_PROVIDERS.find((p) => p.value === provider)?.name ?? provider;
48
- info(`Starting ${providerName} login…`);
49
- const server = await startOAuthServer();
50
- const spin = spinner(`Requesting ${providerName} authorization URL…`);
51
- const urlRes = await getOAuthUrl(provider, server.callbackUrl);
52
- spin.stop();
53
- if (!urlRes.success || !urlRes.data?.url) {
54
- server.close();
55
- error(urlRes.error?.message
56
- ? `${providerName} login is not available: ${urlRes.error.message}`
57
- : `Failed to get ${providerName} authorization URL`);
58
- process.exit(1);
59
- }
60
- const authUrl = urlRes.data.url;
61
- console.log('');
62
- console.log(chalk.bold(`Opening ${providerName} login in your browser…`));
63
- console.log(chalk.dim(`If the browser doesn't open automatically, visit:`));
64
- console.log(chalk.cyan(authUrl));
65
- console.log('');
66
- info('Waiting for you to complete authentication in the browser…');
67
- info(chalk.dim('(Press Ctrl+C to cancel)'));
68
- console.log('');
69
- openBrowser(authUrl);
70
- const result = await server.waitForCallback();
71
- if (result.error) {
72
- error(`Login failed: ${result.error}`);
73
- process.exit(1);
74
- }
75
- if (!result.accessToken) {
76
- warn('No access token found in the callback response.');
77
- console.log(chalk.dim('Raw callback parameters:'));
78
- for (const [k, v] of Object.entries(result.rawParams)) {
79
- console.log(chalk.dim(` ${k}: ${v}`));
80
- }
81
- error('Please check if the API returned the token in a different format.');
82
- info('You can try logging in with email instead: minara login --email');
83
- process.exit(1);
84
- }
85
- saveCredentials({
86
- accessToken: result.accessToken,
87
- userId: result.userId,
88
- email: result.email,
89
- displayName: result.displayName,
90
- });
91
- // Fetch full user info if only token was returned
92
- if (!result.email && !result.displayName) {
93
- const spin2 = spinner('Fetching account info…');
94
- const meRes = await getCurrentUser(result.accessToken);
95
- spin2.stop();
96
- if (meRes.success && meRes.data) {
97
- saveCredentials({
98
- accessToken: result.accessToken,
99
- userId: meRes.data.id,
100
- email: meRes.data.email,
101
- displayName: meRes.data.displayName,
102
- });
103
- }
104
- }
105
- success(`${providerName} login successful! Credentials saved to ~/.minara/`);
106
- if (result.displayName)
107
- console.log(chalk.dim(` Welcome, ${result.displayName}`));
108
- if (result.email)
109
- console.log(chalk.dim(` ${result.email}`));
110
- }
111
43
  // ─── Device login flow (RFC 8628) ─────────────────────────────────────────
112
44
  async function loginWithDevice() {
113
45
  info('Starting device login...');
@@ -171,9 +103,7 @@ async function loginWithDevice() {
171
103
  export const loginCommand = new Command('login')
172
104
  .description('Login to your Minara account')
173
105
  .option('-e, --email <email>', 'Login with email verification code')
174
- .option('--google', 'Login with Google')
175
- .option('--apple', 'Login with Apple ID')
176
- .option('--device', 'Login with device code (for headless environments)')
106
+ .option('--device', 'Login with device code (opens browser)')
177
107
  .action(wrapAction(async (opts) => {
178
108
  // Warn if already logged in
179
109
  const existing = loadCredentials();
@@ -190,12 +120,6 @@ export const loginCommand = new Command('login')
190
120
  if (opts.email) {
191
121
  method = 'email';
192
122
  }
193
- else if (opts.google) {
194
- method = 'google';
195
- }
196
- else if (opts.apple) {
197
- method = 'apple';
198
- }
199
123
  else if (opts.device) {
200
124
  method = 'device';
201
125
  }
@@ -203,10 +127,8 @@ export const loginCommand = new Command('login')
203
127
  method = await select({
204
128
  message: 'How would you like to login?',
205
129
  choices: [
130
+ { name: 'Device code (opens browser to verify)', value: 'device' },
206
131
  { name: 'Email verification code', value: 'email' },
207
- { name: 'Google', value: 'google' },
208
- { name: 'Apple ID', value: 'apple' },
209
- { name: 'Device code (for headless environments)', value: 'device' },
210
132
  ],
211
133
  });
212
134
  }
@@ -214,11 +136,8 @@ export const loginCommand = new Command('login')
214
136
  if (method === 'email') {
215
137
  await loginWithEmail(opts.email);
216
138
  }
217
- else if (method === 'device') {
218
- await loginWithDevice();
219
- }
220
139
  else {
221
- await loginWithOAuth(method);
140
+ await loginWithDevice();
222
141
  }
223
142
  // ── Offer Touch ID setup (macOS only, if not already enabled) ────
224
143
  const config = loadConfig();
@@ -1,10 +1,10 @@
1
1
  import { Command } from 'commander';
2
- import { input, select, confirm } from '@inquirer/prompts';
2
+ import { input, select } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { swaps, swapsSimulate } from '../api/crosschain.js';
5
5
  import { get } from '../api/client.js';
6
6
  import { requireAuth } from '../config.js';
7
- import { success, info, warn, spinner, formatOrderSide, assertApiOk, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel, normalizeChain } from '../utils.js';
7
+ import { success, info, warn, spinner, assertApiOk, wrapAction, requireTransactionConfirmation, lookupToken, normalizeChain } from '../utils.js';
8
8
  import { requireTouchId } from '../touchid.js';
9
9
  import { printTxResult, printKV } from '../formatters.js';
10
10
  export const swapCommand = new Command('swap')
@@ -80,16 +80,7 @@ export const swapCommand = new Command('swap')
80
80
  amount = String(maxBalance);
81
81
  }
82
82
  }
83
- // ── 5. Summary ───────────────────────────────────────────────────────
84
- console.log('');
85
- console.log(chalk.bold('Swap Summary:'));
86
- console.log(` Chain : ${chalk.cyan(chain)}`);
87
- console.log(` Action : ${formatOrderSide(side)}`);
88
- console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
89
- console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
90
- console.log(` Amount : ${chalk.bold(amount)} ${side === 'buy' ? 'USD' : '(token)'}`);
91
- console.log('');
92
- // ── 6. Dry run ───────────────────────────────────────────────────────
83
+ // ── 5. Dry run ───────────────────────────────────────────────────────
93
84
  if (opts.dryRun) {
94
85
  info('Simulating swap (dry-run)…');
95
86
  const spin = spinner('Simulating…');
@@ -111,19 +102,10 @@ export const swapCommand = new Command('swap')
111
102
  }
112
103
  return;
113
104
  }
114
- // ── 7. Confirm ───────────────────────────────────────────────────────
105
+ // ── 7. Confirm & Touch ID ──────────────────────────────────────────
115
106
  if (!opts.yes) {
116
- const confirmed = await confirm({
117
- message: `Confirm ${side.toUpperCase()} swap?`,
118
- default: false,
119
- });
120
- if (!confirmed) {
121
- console.log(chalk.dim('Swap cancelled.'));
122
- return;
123
- }
107
+ await requireTransactionConfirmation(`${side.toUpperCase()} swap · ${amount} ${side === 'buy' ? 'USD' : 'tokens'} · ${chain}`, tokenInfo, { chain, side, amount: `${amount} ${side === 'buy' ? 'USD' : '(token)'}` });
124
108
  }
125
- // ── 8. Transaction confirmation & Touch ID ────────────────────────────
126
- await requireTransactionConfirmation(`${side.toUpperCase()} swap · ${amount} ${side === 'buy' ? 'USD' : 'tokens'} · ${chain}`, tokenInfo, { chain, side, amount: `${amount} ${side === 'buy' ? 'USD' : '(token)'}` });
127
109
  await requireTouchId();
128
110
  // ── 9. Execute ───────────────────────────────────────────────────────
129
111
  const spin = spinner('Executing swap…');
@@ -1,9 +1,8 @@
1
1
  import { Command } from 'commander';
2
- import { input, confirm } from '@inquirer/prompts';
3
- import chalk from 'chalk';
2
+ import { input } from '@inquirer/prompts';
4
3
  import { transfer } from '../api/crosschain.js';
5
4
  import { requireAuth } from '../config.js';
6
- import { success, warn, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
5
+ import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken } from '../utils.js';
7
6
  import { requireTouchId } from '../touchid.js';
8
7
  import { printTxResult } from '../formatters.js';
9
8
  export const transferCommand = new Command('transfer')
@@ -36,25 +35,10 @@ export const transferCommand = new Command('transfer')
36
35
  message: 'Recipient address:',
37
36
  validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
38
37
  });
39
- // ── 5. Summary ───────────────────────────────────────────────────────
40
- console.log('');
41
- console.log(chalk.bold.red('⚠ Transfer Summary:'));
42
- console.log(` Chain : ${chalk.cyan(chain)}`);
43
- console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
44
- console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
45
- console.log(` Amount : ${chalk.bold(amount)}`);
46
- console.log(` To : ${chalk.yellow(recipient)}`);
47
- console.log('');
48
- warn('Transfers cannot be reversed. Double-check the recipient address!');
38
+ // ── 5. Confirm & Touch ID ──────────────────────────────────────────
49
39
  if (!opts.yes) {
50
- const confirmed = await confirm({ message: 'Confirm transfer?', default: false });
51
- if (!confirmed) {
52
- console.log(chalk.dim('Transfer cancelled.'));
53
- return;
54
- }
40
+ await requireTransactionConfirmation(`Transfer ${amount} ${recipient} · ${chain}`, tokenInfo, { chain, amount, destination: recipient });
55
41
  }
56
- // ── 6. Transaction confirmation & Touch ID ────────────────────────────
57
- await requireTransactionConfirmation(`Transfer ${amount} tokens → ${recipient} · ${chain}`, tokenInfo, { chain, amount });
58
42
  await requireTouchId();
59
43
  // ── 7. Execute ───────────────────────────────────────────────────────
60
44
  const spin = spinner('Processing transfer…');
@@ -1,9 +1,9 @@
1
1
  import { Command } from 'commander';
2
- import { input, confirm } from '@inquirer/prompts';
2
+ 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, warn, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
6
+ import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken } 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')
@@ -59,30 +59,10 @@ export const withdrawCommand = new Command('withdraw')
59
59
  message: 'Destination address (your external wallet):',
60
60
  validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
61
61
  });
62
- // ── 6. Summary ───────────────────────────────────────────────────────
63
- console.log('');
64
- console.log(chalk.bold.red('⚠ Withdrawal Summary'));
65
- console.log(` Chain : ${chalk.cyan(chain)}`);
66
- console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
67
- console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
68
- console.log(` Amount : ${chalk.bold(amount)}`);
69
- console.log(` Destination : ${chalk.yellow(recipient)}`);
70
- console.log('');
71
- warn('Withdrawals are irreversible. Please double-check the network and address!');
72
- warn('Sending to the wrong chain or address will result in permanent loss of funds.');
73
- console.log('');
62
+ // ── 6. Confirm & Touch ID ──────────────────────────────────────────
74
63
  if (!opts.yes) {
75
- const confirmed = await confirm({
76
- message: 'I have verified the address and network. Proceed with withdrawal?',
77
- default: false,
78
- });
79
- if (!confirmed) {
80
- console.log(chalk.dim('Withdrawal cancelled.'));
81
- return;
82
- }
64
+ await requireTransactionConfirmation(`Withdraw ${amount} ${recipient} · ${chain}`, tokenInfo, { chain, amount, destination: recipient });
83
65
  }
84
- // ── 7. Transaction confirmation & Touch ID ────────────────────────────
85
- await requireTransactionConfirmation(`Withdraw ${amount} tokens → ${recipient} · ${chain}`, tokenInfo, { chain, amount });
86
66
  await requireTouchId();
87
67
  // ── 8. Execute ───────────────────────────────────────────────────────
88
68
  const spin = spinner('Processing withdrawal…');
package/dist/utils.d.ts CHANGED
@@ -73,6 +73,7 @@ export declare function requireTransactionConfirmation(description: string, toke
73
73
  chain?: string;
74
74
  side?: string;
75
75
  amount?: string;
76
+ destination?: string;
76
77
  }): Promise<void>;
77
78
  /** Open a URL in the user's default browser (cross-platform). */
78
79
  export declare function openBrowser(url: string): void;
package/dist/utils.js CHANGED
@@ -326,6 +326,9 @@ export async function requireTransactionConfirmation(description, token, details
326
326
  if (details?.amount) {
327
327
  console.log(chalk.dim(' Amount : ') + chalk.bold(details.amount));
328
328
  }
329
+ if (details?.destination) {
330
+ console.log(chalk.dim(' To : ') + chalk.yellow(details.destination));
331
+ }
329
332
  console.log(chalk.dim(` Action : ${description}`));
330
333
  console.log('');
331
334
  const ok = await confirm({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
@@ -1,19 +0,0 @@
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>>>;
@@ -1,37 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- import { Command } from 'commander';
2
- export declare const copyTradeCommand: Command;
@@ -1,176 +0,0 @@
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, requireTransactionConfirmation } from '../utils.js';
7
- import { requireTouchId } from '../touchid.js';
8
- import { printTxResult, printTable, COPY_TRADE_COLUMNS } from '../formatters.js';
9
- // ─── create ──────────────────────────────────────────────────────────────
10
- const createCmd = new Command('create')
11
- .description('Create a copy trade bot')
12
- .option('-y, --yes', 'Skip confirmation')
13
- .action(wrapAction(async (opts) => {
14
- const creds = requireAuth();
15
- const chain = await selectChain('Chain:', true);
16
- const targetAddress = await input({
17
- message: 'Target wallet address to copy:',
18
- validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
19
- });
20
- const name = await input({ message: 'Name for this copy trade (optional):' }) || undefined;
21
- const fixedAmount = await numberPrompt({ message: 'Fixed buy amount (USD) per copy:', min: 1, required: true });
22
- const copySell = await confirm({ message: 'Also copy sell actions?', default: true });
23
- let copySellSamePercentage = false;
24
- let copySellQuitPercentage;
25
- if (copySell) {
26
- copySellSamePercentage = await confirm({
27
- message: 'Copy sell with same percentage as target?',
28
- default: true,
29
- });
30
- copySellQuitPercentage = (await numberPrompt({
31
- message: 'Clear position alert % (when target sells >= this %, clear your position; 0 to skip):',
32
- default: 0,
33
- })) || undefined;
34
- }
35
- console.log('');
36
- console.log(chalk.bold('Copy Trade Bot:'));
37
- console.log(` Chain : ${chalk.cyan(chain)}`);
38
- console.log(` Target : ${chalk.yellow(targetAddress)}`);
39
- if (name)
40
- console.log(` Name : ${name}`);
41
- console.log(` Buy Amount : $${fixedAmount}`);
42
- console.log(` Copy Sell : ${copySell ? 'Yes' : 'No'}`);
43
- if (copySellSamePercentage)
44
- console.log(` Same % : Yes`);
45
- if (copySellQuitPercentage)
46
- console.log(` Quit Threshold : ${copySellQuitPercentage}%`);
47
- console.log('');
48
- if (!opts.yes) {
49
- const ok = await confirm({ message: 'Create this copy trade?', default: false });
50
- if (!ok)
51
- return;
52
- }
53
- await requireTransactionConfirmation(`Copy trade · $${fixedAmount}/trade · target ${targetAddress} · ${chain}`);
54
- await requireTouchId();
55
- const spin = spinner('Creating copy trade…');
56
- const res = await ctApi.createCopyTrade(creds.accessToken, {
57
- chain, targetAddress, name,
58
- mode: 'fixedAmount',
59
- fixedAmount: fixedAmount,
60
- copySell,
61
- copySellSamePercentage,
62
- copySellQuitPercentage,
63
- });
64
- spin.stop();
65
- assertApiOk(res, 'Failed to create copy trade');
66
- success('Copy trade created!');
67
- printTxResult(res.data);
68
- }));
69
- // ─── list ────────────────────────────────────────────────────────────────
70
- const listCmd = new Command('list')
71
- .alias('ls')
72
- .description('List your copy trades')
73
- .action(wrapAction(async () => {
74
- const creds = requireAuth();
75
- const spin = spinner('Fetching copy trades…');
76
- const res = await ctApi.listCopyTrades(creds.accessToken);
77
- spin.stop();
78
- assertApiOk(res, 'Failed to fetch copy trades');
79
- const data = res.data;
80
- if (!data || data.length === 0) {
81
- console.log(chalk.dim('No copy trades.'));
82
- return;
83
- }
84
- console.log('');
85
- console.log(chalk.bold('Copy Trades:'));
86
- printTable(data, COPY_TRADE_COLUMNS);
87
- console.log('');
88
- }));
89
- // ─── start / stop ────────────────────────────────────────────────────────
90
- async function pickCopyTrade(token) {
91
- const spin = spinner('Fetching copy trades…');
92
- const res = await ctApi.listCopyTrades(token);
93
- spin.stop();
94
- const trades = res.data;
95
- if (!trades || trades.length === 0) {
96
- info('No copy trades found.');
97
- process.exit(0);
98
- }
99
- return select({
100
- message: 'Select copy trade:',
101
- choices: trades.map((t) => ({
102
- name: `[${t.id.slice(0, 12)}…] ${t.name ?? t.targetAddress} status=${t.status ?? '?'}`,
103
- value: t.id,
104
- })),
105
- });
106
- }
107
- const startCmd = new Command('start')
108
- .description('Start (resume) a copy trade')
109
- .argument('[id]', 'Copy trade ID')
110
- .action(wrapAction(async (idArg) => {
111
- const creds = requireAuth();
112
- const id = idArg ?? await pickCopyTrade(creds.accessToken);
113
- const spin = spinner('Starting…');
114
- const res = await ctApi.startCopyTrade(creds.accessToken, id);
115
- spin.stop();
116
- assertApiOk(res, 'Failed to start copy trade');
117
- success('Copy trade started.');
118
- }));
119
- const stopCmd = new Command('stop')
120
- .description('Stop (pause) a copy trade')
121
- .argument('[id]', 'Copy trade ID')
122
- .action(wrapAction(async (idArg) => {
123
- const creds = requireAuth();
124
- const id = idArg ?? await pickCopyTrade(creds.accessToken);
125
- const ok = await confirm({ message: `Stop copy trade ${id.slice(0, 12)}…?`, default: false });
126
- if (!ok)
127
- return;
128
- const spin = spinner('Stopping…');
129
- const res = await ctApi.stopCopyTrade(creds.accessToken, id);
130
- spin.stop();
131
- assertApiOk(res, 'Failed to stop copy trade');
132
- success('Copy trade stopped.');
133
- }));
134
- // ─── delete ──────────────────────────────────────────────────────────────
135
- const deleteCmd = new Command('delete')
136
- .description('Delete a copy trade')
137
- .argument('[id]', 'Copy trade ID')
138
- .option('-y, --yes', 'Skip confirmation')
139
- .action(wrapAction(async (idArg, opts) => {
140
- const creds = requireAuth();
141
- const id = idArg ?? await pickCopyTrade(creds.accessToken);
142
- if (!opts?.yes) {
143
- const ok = await confirm({ message: `Delete copy trade ${id.slice(0, 12)}…? This cannot be undone.`, default: false });
144
- if (!ok)
145
- return;
146
- }
147
- const spin = spinner('Deleting…');
148
- const res = await ctApi.deleteCopyTrade(creds.accessToken, id);
149
- spin.stop();
150
- assertApiOk(res, 'Failed to delete copy trade');
151
- success('Copy trade deleted.');
152
- }));
153
- // ─── parent ──────────────────────────────────────────────────────────────
154
- export const copyTradeCommand = new Command('copy-trade')
155
- .alias('ct')
156
- .description('Copy trading — follow wallet addresses')
157
- .addCommand(createCmd)
158
- .addCommand(listCmd)
159
- .addCommand(startCmd)
160
- .addCommand(stopCmd)
161
- .addCommand(deleteCmd)
162
- .action(wrapAction(async () => {
163
- const action = await select({
164
- message: 'Copy Trade:',
165
- choices: [
166
- { name: 'Create a new copy trade', value: 'create' },
167
- { name: 'List copy trades', value: 'list' },
168
- { name: 'Start a copy trade', value: 'start' },
169
- { name: 'Stop a copy trade', value: 'stop' },
170
- { name: 'Delete a copy trade', value: 'delete' },
171
- ],
172
- });
173
- const sub = copyTradeCommand.commands.find((c) => c.name() === action);
174
- if (sub)
175
- await sub.parseAsync([], { from: 'user' });
176
- }));