minara 0.1.5 → 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/LICENSE +21 -0
- package/README.md +176 -77
- 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 +115 -11
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +43 -0
- 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 +134 -59
- 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 +417 -0
- package/dist/commands/swap.js +80 -22
- 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 +54 -0
- package/dist/formatters.js +384 -0
- package/dist/index.js +13 -3
- package/dist/touchid.d.ts +18 -0
- package/dist/touchid.js +181 -0
- package/dist/types.d.ts +55 -41
- package/dist/utils.d.ts +44 -0
- package/dist/utils.js +224 -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: {
|
|
@@ -246,41 +256,6 @@ export interface UpdateLimitOrderDto {
|
|
|
246
256
|
targetPrice?: number;
|
|
247
257
|
expiredAt?: number;
|
|
248
258
|
}
|
|
249
|
-
export interface CreateCopyTradeDto {
|
|
250
|
-
targetAddress: string;
|
|
251
|
-
chain: string;
|
|
252
|
-
name?: string;
|
|
253
|
-
mode?: 'fixedAmount';
|
|
254
|
-
copySell?: boolean;
|
|
255
|
-
copySellSamePercentage?: boolean;
|
|
256
|
-
copySellQuitPercentage?: number;
|
|
257
|
-
fixedAmount?: number;
|
|
258
|
-
status?: 'running' | 'paused';
|
|
259
|
-
expiredAt?: number;
|
|
260
|
-
}
|
|
261
|
-
export interface CopyTradeInfo {
|
|
262
|
-
id: string;
|
|
263
|
-
name?: string;
|
|
264
|
-
targetAddress: string;
|
|
265
|
-
chain: string;
|
|
266
|
-
mode?: string;
|
|
267
|
-
fixedAmount?: number;
|
|
268
|
-
copySell?: boolean;
|
|
269
|
-
status?: string;
|
|
270
|
-
createdAt?: string;
|
|
271
|
-
}
|
|
272
|
-
export interface UpdateCopyTradeDto {
|
|
273
|
-
chain: string;
|
|
274
|
-
name?: string;
|
|
275
|
-
mode?: 'fixedAmount';
|
|
276
|
-
copySell?: boolean;
|
|
277
|
-
copySellSamePercentage?: boolean;
|
|
278
|
-
copySellQuitPercentage?: number;
|
|
279
|
-
fixedAmount?: number;
|
|
280
|
-
targetAddress?: string;
|
|
281
|
-
status?: 'running' | 'paused';
|
|
282
|
-
expiredAt?: number;
|
|
283
|
-
}
|
|
284
259
|
export interface UserTradeConfig {
|
|
285
260
|
slippage?: string;
|
|
286
261
|
priorityFee?: string;
|
|
@@ -292,9 +267,15 @@ export interface TokenInfo {
|
|
|
292
267
|
name?: string;
|
|
293
268
|
address?: string;
|
|
294
269
|
chain?: string;
|
|
270
|
+
logo?: string;
|
|
271
|
+
description?: string;
|
|
295
272
|
price?: number;
|
|
296
|
-
|
|
273
|
+
priceChange24H?: number;
|
|
274
|
+
volume?: number;
|
|
275
|
+
volume24H?: number;
|
|
297
276
|
marketCap?: number;
|
|
277
|
+
fdv?: number;
|
|
278
|
+
socialUrls?: Record<string, string>;
|
|
298
279
|
}
|
|
299
280
|
export interface DiscoverEvent {
|
|
300
281
|
id: string;
|
|
@@ -304,11 +285,44 @@ export interface DiscoverEvent {
|
|
|
304
285
|
createdAt?: string;
|
|
305
286
|
}
|
|
306
287
|
export interface PaymentPlan {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
288
|
+
_id: string;
|
|
289
|
+
id: number;
|
|
290
|
+
name: string;
|
|
291
|
+
price: number;
|
|
292
|
+
status: string;
|
|
293
|
+
interval: 'month' | 'year';
|
|
294
|
+
rules: {
|
|
295
|
+
limitDegenMode?: number;
|
|
296
|
+
limitDeepresearch?: number;
|
|
297
|
+
limitCredit?: string;
|
|
298
|
+
limitWorkflows?: number;
|
|
299
|
+
};
|
|
300
|
+
stripePriceId?: string;
|
|
301
|
+
inviteCount?: number;
|
|
302
|
+
}
|
|
303
|
+
export interface CreditPackage {
|
|
304
|
+
_id: string;
|
|
305
|
+
id: number;
|
|
306
|
+
amount: number;
|
|
307
|
+
degenMode: number;
|
|
308
|
+
stripePriceId: string;
|
|
309
|
+
credit: string;
|
|
310
|
+
}
|
|
311
|
+
export interface PlansResponse {
|
|
312
|
+
plans: PaymentPlan[];
|
|
313
|
+
packages: CreditPackage[];
|
|
314
|
+
}
|
|
315
|
+
export interface CheckoutSession {
|
|
316
|
+
url?: string;
|
|
317
|
+
sessionId?: string;
|
|
318
|
+
[key: string]: unknown;
|
|
319
|
+
}
|
|
320
|
+
export interface CryptoCheckout {
|
|
321
|
+
url?: string;
|
|
322
|
+
address?: string;
|
|
323
|
+
amount?: number;
|
|
310
324
|
currency?: string;
|
|
311
|
-
|
|
325
|
+
[key: string]: unknown;
|
|
312
326
|
}
|
|
313
327
|
export interface GasFeeInfo {
|
|
314
328
|
chain?: string;
|
package/dist/utils.d.ts
CHANGED
|
@@ -30,5 +30,49 @@ 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
|
+
chain?: string;
|
|
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;
|
|
44
|
+
/**
|
|
45
|
+
* Look up token metadata by address, ticker, or name.
|
|
46
|
+
*
|
|
47
|
+
* - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
|
|
48
|
+
* search. Results are filtered to those whose symbol matches (case-insensitive).
|
|
49
|
+
* - Otherwise the raw input is sent to the search API and may match by
|
|
50
|
+
* address, ticker, or name.
|
|
51
|
+
*
|
|
52
|
+
* When multiple candidates remain the user is prompted to disambiguate.
|
|
53
|
+
* The returned `address` is always the resolved contract address.
|
|
54
|
+
*/
|
|
55
|
+
export declare function lookupToken(tokenInput: string): Promise<TokenDisplayInfo>;
|
|
56
|
+
/**
|
|
57
|
+
* Format token info for display: "$BONK — Bonk" or just the address when
|
|
58
|
+
* symbol/name are unavailable.
|
|
59
|
+
*/
|
|
60
|
+
export declare function formatTokenLabel(token: TokenDisplayInfo): string;
|
|
61
|
+
/**
|
|
62
|
+
* Prompt the user for a mandatory second confirmation before executing a
|
|
63
|
+
* fund-related operation. Controlled by `confirmBeforeTransaction` in config
|
|
64
|
+
* (default: enabled). This confirmation is independent of the `-y` flag and
|
|
65
|
+
* Touch ID — it serves as an extra safety net.
|
|
66
|
+
*
|
|
67
|
+
* @param description Short one-line summary of the operation.
|
|
68
|
+
* @param token Optional token metadata to highlight in the prompt.
|
|
69
|
+
*
|
|
70
|
+
* Exits the process if the user declines.
|
|
71
|
+
*/
|
|
72
|
+
export declare function requireTransactionConfirmation(description: string, token?: TokenDisplayInfo, details?: {
|
|
73
|
+
chain?: string;
|
|
74
|
+
side?: string;
|
|
75
|
+
amount?: string;
|
|
76
|
+
}): Promise<void>;
|
|
33
77
|
/** Open a URL in the user's default browser (cross-platform). */
|
|
34
78
|
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,228 @@ export function wrapAction(fn) {
|
|
|
114
115
|
}
|
|
115
116
|
};
|
|
116
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
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Look up token metadata by address, ticker, or name.
|
|
197
|
+
*
|
|
198
|
+
* - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
|
|
199
|
+
* search. Results are filtered to those whose symbol matches (case-insensitive).
|
|
200
|
+
* - Otherwise the raw input is sent to the search API and may match by
|
|
201
|
+
* address, ticker, or name.
|
|
202
|
+
*
|
|
203
|
+
* When multiple candidates remain the user is prompted to disambiguate.
|
|
204
|
+
* The returned `address` is always the resolved contract address.
|
|
205
|
+
*/
|
|
206
|
+
export async function lookupToken(tokenInput) {
|
|
207
|
+
const isTicker = tokenInput.startsWith('$');
|
|
208
|
+
const keyword = isTicker ? tokenInput.slice(1) : tokenInput;
|
|
209
|
+
const spin = spinner('Looking up token…');
|
|
210
|
+
try {
|
|
211
|
+
const { searchTokens } = await import('./api/tokens.js');
|
|
212
|
+
const res = await searchTokens(keyword);
|
|
213
|
+
spin.stop();
|
|
214
|
+
if (!res.success || !res.data || res.data.length === 0) {
|
|
215
|
+
if (isTicker) {
|
|
216
|
+
warn(`No token found for ticker $${keyword}`);
|
|
217
|
+
}
|
|
218
|
+
return { address: tokenInput };
|
|
219
|
+
}
|
|
220
|
+
let tokens = res.data;
|
|
221
|
+
if (isTicker) {
|
|
222
|
+
const filtered = tokens.filter((t) => t.symbol?.toLowerCase() === keyword.toLowerCase());
|
|
223
|
+
if (filtered.length > 0)
|
|
224
|
+
tokens = filtered;
|
|
225
|
+
}
|
|
226
|
+
if (!isTicker) {
|
|
227
|
+
const exact = tokens.find((t) => t.address?.toLowerCase() === keyword.toLowerCase());
|
|
228
|
+
if (exact) {
|
|
229
|
+
return { symbol: exact.symbol, name: exact.name ?? displayChain(exact.chain), address: exact.address ?? resolveNativeAddress(exact.chain), chain: exact.chain };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (tokens.length === 1) {
|
|
233
|
+
const t = tokens[0];
|
|
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
|
+
};
|
|
258
|
+
}
|
|
259
|
+
info(`Found ${tokens.length} tokens matching "${tokenInput}"`);
|
|
260
|
+
const selected = await select({
|
|
261
|
+
message: 'Select the correct token:',
|
|
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
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
symbol: selected.symbol,
|
|
274
|
+
name: selected.name ?? displayChain(selected.chain),
|
|
275
|
+
address: selected.address ?? resolveNativeAddress(selected.chain),
|
|
276
|
+
chain: selected.chain,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
spin.stop();
|
|
281
|
+
return { address: tokenInput };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Format token info for display: "$BONK — Bonk" or just the address when
|
|
286
|
+
* symbol/name are unavailable.
|
|
287
|
+
*/
|
|
288
|
+
export function formatTokenLabel(token) {
|
|
289
|
+
if (!token.symbol)
|
|
290
|
+
return chalk.yellow(token.address);
|
|
291
|
+
const ticker = chalk.bold('$' + token.symbol);
|
|
292
|
+
return token.name ? `${ticker} ${chalk.dim('—')} ${token.name}` : ticker;
|
|
293
|
+
}
|
|
294
|
+
// ─── Transaction confirmation ─────────────────────────────────────────────────
|
|
295
|
+
/**
|
|
296
|
+
* Prompt the user for a mandatory second confirmation before executing a
|
|
297
|
+
* fund-related operation. Controlled by `confirmBeforeTransaction` in config
|
|
298
|
+
* (default: enabled). This confirmation is independent of the `-y` flag and
|
|
299
|
+
* Touch ID — it serves as an extra safety net.
|
|
300
|
+
*
|
|
301
|
+
* @param description Short one-line summary of the operation.
|
|
302
|
+
* @param token Optional token metadata to highlight in the prompt.
|
|
303
|
+
*
|
|
304
|
+
* Exits the process if the user declines.
|
|
305
|
+
*/
|
|
306
|
+
export async function requireTransactionConfirmation(description, token, details) {
|
|
307
|
+
const config = loadConfig();
|
|
308
|
+
if (config.confirmBeforeTransaction === false)
|
|
309
|
+
return;
|
|
310
|
+
console.log('');
|
|
311
|
+
console.log(chalk.yellow('⚠'), chalk.bold('Transaction confirmation'));
|
|
312
|
+
if (details?.chain) {
|
|
313
|
+
console.log(chalk.dim(' Chain : ') + chalk.cyan(details.chain));
|
|
314
|
+
}
|
|
315
|
+
if (token) {
|
|
316
|
+
const ticker = token.symbol ? '$' + token.symbol : undefined;
|
|
317
|
+
const label = [ticker, token.name].filter(Boolean).join(' — ');
|
|
318
|
+
console.log(chalk.dim(' Token : ') + (label ? chalk.bold(label) : chalk.dim('Unknown token')));
|
|
319
|
+
console.log(chalk.dim(' Address : ') + chalk.yellow(token.address));
|
|
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
|
+
}
|
|
329
|
+
console.log(chalk.dim(` Action : ${description}`));
|
|
330
|
+
console.log('');
|
|
331
|
+
const ok = await confirm({
|
|
332
|
+
message: 'Are you sure you want to proceed?',
|
|
333
|
+
default: false,
|
|
334
|
+
});
|
|
335
|
+
if (!ok) {
|
|
336
|
+
console.log(chalk.dim('Operation cancelled.'));
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
117
340
|
// ─── Browser ──────────────────────────────────────────────────────────────────
|
|
118
341
|
/** Open a URL in the user's default browser (cross-platform). */
|
|
119
342
|
export function openBrowser(url) {
|