minara 0.1.5 → 0.2.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/LICENSE +21 -0
- package/README.md +172 -65
- package/dist/api/payment.d.ts +17 -0
- package/dist/api/payment.js +39 -0
- package/dist/api/perps.d.ts +2 -0
- package/dist/api/perps.js +4 -0
- package/dist/api/tokens.d.ts +4 -0
- package/dist/api/tokens.js +8 -0
- package/dist/commands/assets.js +89 -8
- package/dist/commands/chat.js +77 -53
- package/dist/commands/config.js +82 -5
- package/dist/commands/copy-trade.js +10 -4
- package/dist/commands/deposit.js +5 -1
- package/dist/commands/discover.js +31 -4
- package/dist/commands/limit-order.js +16 -8
- package/dist/commands/login.js +17 -1
- package/dist/commands/perps.js +48 -13
- package/dist/commands/premium.d.ts +2 -0
- package/dist/commands/premium.js +398 -0
- package/dist/commands/swap.js +29 -13
- package/dist/commands/transfer.js +17 -11
- package/dist/commands/withdraw.js +17 -11
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/formatters.d.ts +56 -0
- package/dist/formatters.js +376 -0
- package/dist/index.js +11 -1
- package/dist/touchid.d.ts +18 -0
- package/dist/touchid.js +181 -0
- package/dist/types.d.ts +55 -6
- package/dist/utils.d.ts +34 -0
- package/dist/utils.js +107 -1
- package/package.json +1 -1
package/dist/touchid.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// Touch ID (macOS) — biometric verification for sensitive operations
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, writeFileSync, unlinkSync, chmodSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { platform } from 'node:os';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { loadConfig, getMinaraDir } from './config.js';
|
|
10
|
+
// Bump this when SWIFT_SOURCE changes to force recompilation
|
|
11
|
+
const TOUCHID_BINARY_VERSION = '1';
|
|
12
|
+
const SWIFT_SOURCE = `
|
|
13
|
+
import Foundation
|
|
14
|
+
import LocalAuthentication
|
|
15
|
+
|
|
16
|
+
// Mode: "check" = just check availability, "auth" = perform authentication
|
|
17
|
+
let mode = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "auth"
|
|
18
|
+
let context = LAContext()
|
|
19
|
+
var error: NSError?
|
|
20
|
+
|
|
21
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
|
22
|
+
let msg = error?.localizedDescription ?? "Touch ID not available"
|
|
23
|
+
fputs("unavailable:\\(msg)\\n", stderr)
|
|
24
|
+
exit(2)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if mode == "check" {
|
|
28
|
+
print("available")
|
|
29
|
+
exit(0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Auth mode — prompt for Touch ID
|
|
33
|
+
let reason = CommandLine.arguments.count > 2
|
|
34
|
+
? CommandLine.arguments[2]
|
|
35
|
+
: "Minara CLI: Authorize transaction"
|
|
36
|
+
|
|
37
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
38
|
+
var authSuccess = false
|
|
39
|
+
|
|
40
|
+
context.evaluatePolicy(
|
|
41
|
+
.deviceOwnerAuthenticationWithBiometrics,
|
|
42
|
+
localizedReason: reason
|
|
43
|
+
) { result, authError in
|
|
44
|
+
authSuccess = result
|
|
45
|
+
if !result {
|
|
46
|
+
let msg = authError?.localizedDescription ?? "Authentication failed"
|
|
47
|
+
fputs("failed:\\(msg)\\n", stderr)
|
|
48
|
+
}
|
|
49
|
+
semaphore.signal()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
semaphore.wait()
|
|
53
|
+
exit(authSuccess ? 0 : 1)
|
|
54
|
+
`;
|
|
55
|
+
// ─── Binary management ───────────────────────────────────────────────────
|
|
56
|
+
const BINARY_NAME = 'touchid_auth';
|
|
57
|
+
const VERSION_FILE = 'touchid_auth.version';
|
|
58
|
+
function getBinaryPath() {
|
|
59
|
+
return join(getMinaraDir(), BINARY_NAME);
|
|
60
|
+
}
|
|
61
|
+
function getVersionPath() {
|
|
62
|
+
return join(getMinaraDir(), VERSION_FILE);
|
|
63
|
+
}
|
|
64
|
+
function isBinaryUpToDate() {
|
|
65
|
+
const binaryPath = getBinaryPath();
|
|
66
|
+
const versionPath = getVersionPath();
|
|
67
|
+
if (!existsSync(binaryPath) || !existsSync(versionPath))
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
const ver = readFileSync(versionPath, 'utf-8').trim();
|
|
71
|
+
return ver === TOUCHID_BINARY_VERSION;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Compile the Touch ID Swift helper binary (cached in ~/.minara/).
|
|
79
|
+
* Returns the path to the compiled binary.
|
|
80
|
+
*/
|
|
81
|
+
function ensureBinary() {
|
|
82
|
+
const binaryPath = getBinaryPath();
|
|
83
|
+
if (isBinaryUpToDate())
|
|
84
|
+
return binaryPath;
|
|
85
|
+
const dir = getMinaraDir();
|
|
86
|
+
const sourcePath = join(dir, 'touchid_auth.swift');
|
|
87
|
+
writeFileSync(sourcePath, SWIFT_SOURCE, 'utf-8');
|
|
88
|
+
try {
|
|
89
|
+
execFileSync('/usr/bin/swiftc', [
|
|
90
|
+
'-O', // optimized build
|
|
91
|
+
'-o', binaryPath,
|
|
92
|
+
sourcePath,
|
|
93
|
+
], {
|
|
94
|
+
timeout: 120_000, // 2 min timeout for first compile
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
});
|
|
97
|
+
chmodSync(binaryPath, 0o700);
|
|
98
|
+
writeFileSync(getVersionPath(), TOUCHID_BINARY_VERSION, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// Clean up on failure
|
|
102
|
+
if (existsSync(binaryPath))
|
|
103
|
+
unlinkSync(binaryPath);
|
|
104
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
105
|
+
throw new Error(`Failed to compile Touch ID helper: ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (existsSync(sourcePath))
|
|
109
|
+
unlinkSync(sourcePath);
|
|
110
|
+
}
|
|
111
|
+
return binaryPath;
|
|
112
|
+
}
|
|
113
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Check whether Touch ID hardware is available on this machine.
|
|
116
|
+
* Returns `false` on non-macOS or when hardware is absent / not enrolled.
|
|
117
|
+
*/
|
|
118
|
+
export function isTouchIdAvailable() {
|
|
119
|
+
if (platform() !== 'darwin')
|
|
120
|
+
return false;
|
|
121
|
+
try {
|
|
122
|
+
const binary = ensureBinary();
|
|
123
|
+
execFileSync(binary, ['check'], { timeout: 10_000, stdio: 'pipe' });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Perform a Touch ID verification.
|
|
132
|
+
* Resolves on success, throws on failure or cancellation.
|
|
133
|
+
*/
|
|
134
|
+
export function verifyTouchId(reason) {
|
|
135
|
+
const binary = ensureBinary();
|
|
136
|
+
const args = ['auth'];
|
|
137
|
+
if (reason)
|
|
138
|
+
args.push(reason);
|
|
139
|
+
execFileSync(binary, args, { timeout: 60_000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* If Touch ID is enabled in config, prompt the user for biometric
|
|
143
|
+
* verification. Exits the process on failure.
|
|
144
|
+
*
|
|
145
|
+
* Call this before any sensitive financial operation.
|
|
146
|
+
* On non-macOS platforms a warning is shown and execution continues.
|
|
147
|
+
*/
|
|
148
|
+
export async function requireTouchId() {
|
|
149
|
+
const config = loadConfig();
|
|
150
|
+
if (!config.touchId)
|
|
151
|
+
return; // Touch ID not enabled — no-op
|
|
152
|
+
if (platform() !== 'darwin') {
|
|
153
|
+
console.log(chalk.yellow('⚠'), 'Touch ID is only available on macOS. Skipping biometric check.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(chalk.blue('🔐'), chalk.bold('Touch ID verification required'));
|
|
158
|
+
try {
|
|
159
|
+
verifyTouchId();
|
|
160
|
+
console.log(chalk.green('✔'), 'Touch ID verified');
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
// Parse stderr for details
|
|
165
|
+
const stderr = (err && typeof err === 'object' && 'stderr' in err)
|
|
166
|
+
? err.stderr?.toString().trim()
|
|
167
|
+
: '';
|
|
168
|
+
if (stderr.startsWith('unavailable:')) {
|
|
169
|
+
console.error(chalk.red('✖'), 'Touch ID is not available on this device.');
|
|
170
|
+
console.error(chalk.dim(' Tip: Run `minara config` to disable Touch ID protection.'));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.error(chalk.red('✖'), 'Touch ID verification failed. Operation cancelled.');
|
|
174
|
+
if (stderr) {
|
|
175
|
+
const detail = stderr.startsWith('failed:') ? stderr.slice(7) : stderr;
|
|
176
|
+
console.error(chalk.dim(` ${detail}`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,17 @@ export interface AuthUser {
|
|
|
32
32
|
accounts?: Record<string, unknown>;
|
|
33
33
|
invitationCode?: string;
|
|
34
34
|
mfaSettings?: Record<string, unknown>;
|
|
35
|
+
subscription?: {
|
|
36
|
+
planId?: number;
|
|
37
|
+
planName?: string;
|
|
38
|
+
status?: string;
|
|
39
|
+
interval?: string;
|
|
40
|
+
currentPeriodEnd?: string;
|
|
41
|
+
cancelAtPeriodEnd?: boolean;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
};
|
|
44
|
+
plan?: Record<string, unknown>;
|
|
45
|
+
[key: string]: unknown;
|
|
35
46
|
}
|
|
36
47
|
export interface DeviceAuthStartResponse {
|
|
37
48
|
device_code: string;
|
|
@@ -68,7 +79,6 @@ export interface ChatRequestDTO {
|
|
|
68
79
|
chatId?: string;
|
|
69
80
|
parentMessageId?: string;
|
|
70
81
|
thinking?: boolean;
|
|
71
|
-
deepresearch?: boolean;
|
|
72
82
|
workMode?: string;
|
|
73
83
|
platform?: string;
|
|
74
84
|
message: {
|
|
@@ -292,9 +302,15 @@ export interface TokenInfo {
|
|
|
292
302
|
name?: string;
|
|
293
303
|
address?: string;
|
|
294
304
|
chain?: string;
|
|
305
|
+
logo?: string;
|
|
306
|
+
description?: string;
|
|
295
307
|
price?: number;
|
|
296
|
-
|
|
308
|
+
priceChange24H?: number;
|
|
309
|
+
volume?: number;
|
|
310
|
+
volume24H?: number;
|
|
297
311
|
marketCap?: number;
|
|
312
|
+
fdv?: number;
|
|
313
|
+
socialUrls?: Record<string, string>;
|
|
298
314
|
}
|
|
299
315
|
export interface DiscoverEvent {
|
|
300
316
|
id: string;
|
|
@@ -304,11 +320,44 @@ export interface DiscoverEvent {
|
|
|
304
320
|
createdAt?: string;
|
|
305
321
|
}
|
|
306
322
|
export interface PaymentPlan {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
_id: string;
|
|
324
|
+
id: number;
|
|
325
|
+
name: string;
|
|
326
|
+
price: number;
|
|
327
|
+
status: string;
|
|
328
|
+
interval: 'month' | 'year';
|
|
329
|
+
rules: {
|
|
330
|
+
limitDegenMode?: number;
|
|
331
|
+
limitDeepresearch?: number;
|
|
332
|
+
limitCredit?: string;
|
|
333
|
+
limitWorkflows?: number;
|
|
334
|
+
};
|
|
335
|
+
stripePriceId?: string;
|
|
336
|
+
inviteCount?: number;
|
|
337
|
+
}
|
|
338
|
+
export interface CreditPackage {
|
|
339
|
+
_id: string;
|
|
340
|
+
id: number;
|
|
341
|
+
amount: number;
|
|
342
|
+
degenMode: number;
|
|
343
|
+
stripePriceId: string;
|
|
344
|
+
credit: string;
|
|
345
|
+
}
|
|
346
|
+
export interface PlansResponse {
|
|
347
|
+
plans: PaymentPlan[];
|
|
348
|
+
packages: CreditPackage[];
|
|
349
|
+
}
|
|
350
|
+
export interface CheckoutSession {
|
|
351
|
+
url?: string;
|
|
352
|
+
sessionId?: string;
|
|
353
|
+
[key: string]: unknown;
|
|
354
|
+
}
|
|
355
|
+
export interface CryptoCheckout {
|
|
356
|
+
url?: string;
|
|
357
|
+
address?: string;
|
|
358
|
+
amount?: number;
|
|
310
359
|
currency?: string;
|
|
311
|
-
|
|
360
|
+
[key: string]: unknown;
|
|
312
361
|
}
|
|
313
362
|
export interface GasFeeInfo {
|
|
314
363
|
chain?: string;
|
package/dist/utils.d.ts
CHANGED
|
@@ -30,5 +30,39 @@ export declare function selectChain(message?: string, compact?: boolean): Promis
|
|
|
30
30
|
* prints a clean error instead of a raw stack trace.
|
|
31
31
|
*/
|
|
32
32
|
export declare function wrapAction<A extends unknown[]>(fn: (...args: A) => Promise<void>): (...args: A) => Promise<void>;
|
|
33
|
+
export interface TokenDisplayInfo {
|
|
34
|
+
symbol?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
address: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Look up token metadata by address, ticker, or name.
|
|
40
|
+
*
|
|
41
|
+
* - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
|
|
42
|
+
* search. Results are filtered to those whose symbol matches (case-insensitive).
|
|
43
|
+
* - Otherwise the raw input is sent to the search API and may match by
|
|
44
|
+
* address, ticker, or name.
|
|
45
|
+
*
|
|
46
|
+
* When multiple candidates remain the user is prompted to disambiguate.
|
|
47
|
+
* The returned `address` is always the resolved contract address.
|
|
48
|
+
*/
|
|
49
|
+
export declare function lookupToken(tokenInput: string): Promise<TokenDisplayInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* Format token info for display: "$BONK — Bonk" or just the address when
|
|
52
|
+
* symbol/name are unavailable.
|
|
53
|
+
*/
|
|
54
|
+
export declare function formatTokenLabel(token: TokenDisplayInfo): string;
|
|
55
|
+
/**
|
|
56
|
+
* Prompt the user for a mandatory second confirmation before executing a
|
|
57
|
+
* fund-related operation. Controlled by `confirmBeforeTransaction` in config
|
|
58
|
+
* (default: enabled). This confirmation is independent of the `-y` flag and
|
|
59
|
+
* Touch ID — it serves as an extra safety net.
|
|
60
|
+
*
|
|
61
|
+
* @param description Short one-line summary of the operation.
|
|
62
|
+
* @param token Optional token metadata to highlight in the prompt.
|
|
63
|
+
*
|
|
64
|
+
* Exits the process if the user declines.
|
|
65
|
+
*/
|
|
66
|
+
export declare function requireTransactionConfirmation(description: string, token?: TokenDisplayInfo): Promise<void>;
|
|
33
67
|
/** Open a URL in the user's default browser (cross-platform). */
|
|
34
68
|
export declare function openBrowser(url: string): void;
|
package/dist/utils.js
CHANGED
|
@@ -2,8 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
4
|
import { platform } from 'node:os';
|
|
5
|
-
import { select } from '@inquirer/prompts';
|
|
5
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
6
6
|
import { SUPPORTED_CHAINS } from './types.js';
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
7
8
|
// ─── Logging helpers ─────────────────────────────────────────────────────────
|
|
8
9
|
export function success(msg) {
|
|
9
10
|
console.log(chalk.green('✔'), msg);
|
|
@@ -114,6 +115,111 @@ export function wrapAction(fn) {
|
|
|
114
115
|
}
|
|
115
116
|
};
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Look up token metadata by address, ticker, or name.
|
|
120
|
+
*
|
|
121
|
+
* - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
|
|
122
|
+
* search. Results are filtered to those whose symbol matches (case-insensitive).
|
|
123
|
+
* - Otherwise the raw input is sent to the search API and may match by
|
|
124
|
+
* address, ticker, or name.
|
|
125
|
+
*
|
|
126
|
+
* When multiple candidates remain the user is prompted to disambiguate.
|
|
127
|
+
* The returned `address` is always the resolved contract address.
|
|
128
|
+
*/
|
|
129
|
+
export async function lookupToken(tokenInput) {
|
|
130
|
+
const isTicker = tokenInput.startsWith('$');
|
|
131
|
+
const keyword = isTicker ? tokenInput.slice(1) : tokenInput;
|
|
132
|
+
const spin = spinner('Looking up token…');
|
|
133
|
+
try {
|
|
134
|
+
const { searchTokens } = await import('./api/tokens.js');
|
|
135
|
+
const res = await searchTokens(keyword);
|
|
136
|
+
spin.stop();
|
|
137
|
+
if (!res.success || !res.data || res.data.length === 0) {
|
|
138
|
+
if (isTicker) {
|
|
139
|
+
warn(`No token found for ticker $${keyword}`);
|
|
140
|
+
}
|
|
141
|
+
return { address: tokenInput };
|
|
142
|
+
}
|
|
143
|
+
let tokens = res.data;
|
|
144
|
+
if (isTicker) {
|
|
145
|
+
const filtered = tokens.filter((t) => t.symbol?.toLowerCase() === keyword.toLowerCase());
|
|
146
|
+
if (filtered.length > 0)
|
|
147
|
+
tokens = filtered;
|
|
148
|
+
}
|
|
149
|
+
if (!isTicker) {
|
|
150
|
+
const exact = tokens.find((t) => t.address?.toLowerCase() === keyword.toLowerCase());
|
|
151
|
+
if (exact) {
|
|
152
|
+
return { symbol: exact.symbol, name: exact.name, address: exact.address ?? tokenInput };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (tokens.length === 1) {
|
|
156
|
+
const t = tokens[0];
|
|
157
|
+
return { symbol: t.symbol, name: t.name, address: t.address ?? tokenInput };
|
|
158
|
+
}
|
|
159
|
+
info(`Found ${tokens.length} tokens matching "${tokenInput}"`);
|
|
160
|
+
const selected = await select({
|
|
161
|
+
message: 'Select the correct token:',
|
|
162
|
+
choices: tokens.map((t) => ({
|
|
163
|
+
name: `${t.symbol ? chalk.bold('$' + t.symbol) : '?'} — ${t.name ?? 'Unknown'}\n ${chalk.yellow(t.address ?? '')}`,
|
|
164
|
+
value: t,
|
|
165
|
+
})),
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
symbol: selected.symbol,
|
|
169
|
+
name: selected.name,
|
|
170
|
+
address: selected.address ?? tokenInput,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
spin.stop();
|
|
175
|
+
return { address: tokenInput };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Format token info for display: "$BONK — Bonk" or just the address when
|
|
180
|
+
* symbol/name are unavailable.
|
|
181
|
+
*/
|
|
182
|
+
export function formatTokenLabel(token) {
|
|
183
|
+
if (!token.symbol)
|
|
184
|
+
return chalk.yellow(token.address);
|
|
185
|
+
const ticker = chalk.bold('$' + token.symbol);
|
|
186
|
+
return token.name ? `${ticker} ${chalk.dim('—')} ${token.name}` : ticker;
|
|
187
|
+
}
|
|
188
|
+
// ─── Transaction confirmation ─────────────────────────────────────────────────
|
|
189
|
+
/**
|
|
190
|
+
* Prompt the user for a mandatory second confirmation before executing a
|
|
191
|
+
* fund-related operation. Controlled by `confirmBeforeTransaction` in config
|
|
192
|
+
* (default: enabled). This confirmation is independent of the `-y` flag and
|
|
193
|
+
* Touch ID — it serves as an extra safety net.
|
|
194
|
+
*
|
|
195
|
+
* @param description Short one-line summary of the operation.
|
|
196
|
+
* @param token Optional token metadata to highlight in the prompt.
|
|
197
|
+
*
|
|
198
|
+
* Exits the process if the user declines.
|
|
199
|
+
*/
|
|
200
|
+
export async function requireTransactionConfirmation(description, token) {
|
|
201
|
+
const config = loadConfig();
|
|
202
|
+
if (config.confirmBeforeTransaction === false)
|
|
203
|
+
return;
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(chalk.yellow('⚠'), chalk.bold('Transaction confirmation'));
|
|
206
|
+
if (token) {
|
|
207
|
+
const ticker = token.symbol ? '$' + token.symbol : undefined;
|
|
208
|
+
const label = [ticker, token.name].filter(Boolean).join(' — ');
|
|
209
|
+
console.log(chalk.dim(' Token : ') + (label ? chalk.bold(label) : chalk.dim('Unknown token')));
|
|
210
|
+
console.log(chalk.dim(' Address : ') + chalk.yellow(token.address));
|
|
211
|
+
}
|
|
212
|
+
console.log(chalk.dim(` Action : ${description}`));
|
|
213
|
+
console.log('');
|
|
214
|
+
const ok = await confirm({
|
|
215
|
+
message: 'Are you sure you want to proceed?',
|
|
216
|
+
default: false,
|
|
217
|
+
});
|
|
218
|
+
if (!ok) {
|
|
219
|
+
console.log(chalk.dim('Operation cancelled.'));
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
117
223
|
// ─── Browser ──────────────────────────────────────────────────────────────────
|
|
118
224
|
/** Open a URL in the user's default browser (cross-platform). */
|
|
119
225
|
export function openBrowser(url) {
|