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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as paymentApi from '../api/payment.js';
|
|
5
|
+
import { getCurrentUser } from '../api/auth.js';
|
|
6
|
+
import { requireAuth } from '../config.js';
|
|
7
|
+
import { success, info, warn, spinner, assertApiOk, wrapAction } from '../utils.js';
|
|
8
|
+
import { openBrowser } from '../utils.js';
|
|
9
|
+
import { printKV, isRawJson } from '../formatters.js';
|
|
10
|
+
// ─── helpers ────────────────────────────────────────────────────────────
|
|
11
|
+
function formatPrice(plan) {
|
|
12
|
+
if (plan.price === 0)
|
|
13
|
+
return chalk.green('Free');
|
|
14
|
+
const price = `$${plan.price}`;
|
|
15
|
+
const period = plan.interval === 'month' ? '/mo' : '/yr';
|
|
16
|
+
return chalk.bold(price) + chalk.dim(period);
|
|
17
|
+
}
|
|
18
|
+
function formatCredits(rules) {
|
|
19
|
+
const credits = rules.limitCredit ? Number(rules.limitCredit).toLocaleString() : '—';
|
|
20
|
+
return credits;
|
|
21
|
+
}
|
|
22
|
+
/** Group plans by tier name and display monthly + yearly side-by-side */
|
|
23
|
+
function groupPlansByTier(plans) {
|
|
24
|
+
const tiers = new Map();
|
|
25
|
+
for (const p of plans) {
|
|
26
|
+
if (p.status !== 'active')
|
|
27
|
+
continue;
|
|
28
|
+
const existing = tiers.get(p.name) ?? {};
|
|
29
|
+
if (p.interval === 'month')
|
|
30
|
+
existing.monthly = p;
|
|
31
|
+
else
|
|
32
|
+
existing.yearly = p;
|
|
33
|
+
tiers.set(p.name, existing);
|
|
34
|
+
}
|
|
35
|
+
return tiers;
|
|
36
|
+
}
|
|
37
|
+
// ─── plans ──────────────────────────────────────────────────────────────
|
|
38
|
+
const plansCmd = new Command('plans')
|
|
39
|
+
.description('View all available subscription plans')
|
|
40
|
+
.action(wrapAction(async () => {
|
|
41
|
+
const spin = spinner('Fetching plans…');
|
|
42
|
+
const res = await paymentApi.getPlans();
|
|
43
|
+
spin.stop();
|
|
44
|
+
assertApiOk(res, 'Failed to fetch plans');
|
|
45
|
+
const { plans, packages } = res.data;
|
|
46
|
+
if (isRawJson()) {
|
|
47
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// ── Subscription Plans ──────────────────────────────────────────
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(chalk.bold('Subscription Plans:'));
|
|
53
|
+
console.log('');
|
|
54
|
+
const tiers = groupPlansByTier(plans);
|
|
55
|
+
// Table header
|
|
56
|
+
const header = [
|
|
57
|
+
chalk.white.bold('Plan'),
|
|
58
|
+
chalk.white.bold('Monthly'),
|
|
59
|
+
chalk.white.bold('Yearly'),
|
|
60
|
+
chalk.white.bold('Credits'),
|
|
61
|
+
chalk.white.bold('Workflows'),
|
|
62
|
+
chalk.white.bold('Invites'),
|
|
63
|
+
];
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const [tierName, tier] of tiers) {
|
|
66
|
+
const ref = tier.monthly ?? tier.yearly;
|
|
67
|
+
const monthly = tier.monthly ? formatPrice(tier.monthly) : chalk.dim('—');
|
|
68
|
+
const yearly = tier.yearly ? formatPrice(tier.yearly) : chalk.dim('—');
|
|
69
|
+
const savings = tier.monthly && tier.yearly
|
|
70
|
+
? chalk.green(` (save ${Math.round((1 - tier.yearly.price / (tier.monthly.price * 12)) * 100)}%)`)
|
|
71
|
+
: '';
|
|
72
|
+
rows.push([
|
|
73
|
+
chalk.bold(tierName),
|
|
74
|
+
monthly,
|
|
75
|
+
yearly + savings,
|
|
76
|
+
formatCredits(ref.rules),
|
|
77
|
+
String(ref.rules.limitWorkflows ?? 0),
|
|
78
|
+
String(ref.inviteCount ?? 0),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
// Print using cli-table3 directly for custom layout
|
|
82
|
+
const Table = (await import('cli-table3')).default;
|
|
83
|
+
const table = new Table({
|
|
84
|
+
head: header,
|
|
85
|
+
style: { head: [], border: ['dim'] },
|
|
86
|
+
});
|
|
87
|
+
for (const row of rows)
|
|
88
|
+
table.push(row);
|
|
89
|
+
console.log(table.toString());
|
|
90
|
+
// ── Credit Packages ─────────────────────────────────────────────
|
|
91
|
+
if (packages.length > 0) {
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.bold('Credit Packages (one-time):'));
|
|
94
|
+
console.log('');
|
|
95
|
+
const pkgTable = new Table({
|
|
96
|
+
head: [
|
|
97
|
+
chalk.white.bold('Price'),
|
|
98
|
+
chalk.white.bold('Credits'),
|
|
99
|
+
],
|
|
100
|
+
style: { head: [], border: ['dim'] },
|
|
101
|
+
});
|
|
102
|
+
for (const pkg of packages) {
|
|
103
|
+
pkgTable.push([
|
|
104
|
+
chalk.bold(`$${pkg.amount}`),
|
|
105
|
+
Number(pkg.credit).toLocaleString(),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
console.log(pkgTable.toString());
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(chalk.dim(' Subscribe: ') + chalk.cyan('minara premium subscribe'));
|
|
112
|
+
console.log('');
|
|
113
|
+
}));
|
|
114
|
+
// ─── status ─────────────────────────────────────────────────────────────
|
|
115
|
+
const statusCmd = new Command('status')
|
|
116
|
+
.description('View your current subscription status')
|
|
117
|
+
.action(wrapAction(async () => {
|
|
118
|
+
const creds = requireAuth();
|
|
119
|
+
const spin = spinner('Fetching subscription status…');
|
|
120
|
+
const [userRes, plansRes] = await Promise.all([
|
|
121
|
+
getCurrentUser(creds.accessToken),
|
|
122
|
+
paymentApi.getPlans(),
|
|
123
|
+
]);
|
|
124
|
+
spin.stop();
|
|
125
|
+
assertApiOk(userRes, 'Failed to fetch account info');
|
|
126
|
+
const user = userRes.data;
|
|
127
|
+
const plans = plansRes.success && plansRes.data ? plansRes.data.plans : [];
|
|
128
|
+
if (isRawJson()) {
|
|
129
|
+
console.log(JSON.stringify({
|
|
130
|
+
subscription: user.subscription ?? null,
|
|
131
|
+
plan: user.plan ?? null,
|
|
132
|
+
}, null, 2));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(chalk.bold('Subscription Status:'));
|
|
137
|
+
console.log('');
|
|
138
|
+
const sub = user.subscription;
|
|
139
|
+
const userPlan = user.plan;
|
|
140
|
+
if (sub && Object.keys(sub).length > 0) {
|
|
141
|
+
const planName = sub.planName ?? sub.plan ?? sub.name ?? '—';
|
|
142
|
+
const planId = sub.planId ?? sub.id;
|
|
143
|
+
// Cross-reference with plans list for interval and credits
|
|
144
|
+
const matchedPlan = planId != null
|
|
145
|
+
? plans.find((p) => p.id === Number(planId) || p._id === String(planId))
|
|
146
|
+
: plans.find((p) => p.name === planName);
|
|
147
|
+
const status = sub.status;
|
|
148
|
+
const interval = sub.interval ?? matchedPlan?.interval;
|
|
149
|
+
const cancelAt = sub.cancelAtPeriodEnd;
|
|
150
|
+
const periodEnd = sub.currentPeriodEnd;
|
|
151
|
+
console.log(` ${chalk.dim('Plan'.padEnd(16))} : ${chalk.bold(String(planName))}`);
|
|
152
|
+
if (status) {
|
|
153
|
+
console.log(` ${chalk.dim('Status'.padEnd(16))} : ${status === 'active' ? chalk.green('Active') : chalk.yellow(status)}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(` ${chalk.dim('Status'.padEnd(16))} : ${chalk.green('Active')}`);
|
|
157
|
+
}
|
|
158
|
+
if (interval) {
|
|
159
|
+
const label = interval === 'month' ? 'Monthly' : interval === 'year' ? 'Yearly' : String(interval);
|
|
160
|
+
console.log(` ${chalk.dim('Billing'.padEnd(16))} : ${label}`);
|
|
161
|
+
}
|
|
162
|
+
if (matchedPlan) {
|
|
163
|
+
console.log(` ${chalk.dim('Price'.padEnd(16))} : ${formatPrice(matchedPlan)}`);
|
|
164
|
+
const credits = formatCredits(matchedPlan.rules);
|
|
165
|
+
if (credits !== '—') {
|
|
166
|
+
console.log(` ${chalk.dim('Credits'.padEnd(16))} : ${credits}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (periodEnd) {
|
|
170
|
+
console.log(` ${chalk.dim('Renews On'.padEnd(16))} : ${new Date(periodEnd).toLocaleDateString()}`);
|
|
171
|
+
}
|
|
172
|
+
if (cancelAt) {
|
|
173
|
+
console.log(` ${chalk.dim(''.padEnd(16))} ${chalk.yellow('⚠ Will cancel at end of billing period')}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (userPlan && Object.keys(userPlan).length > 0) {
|
|
177
|
+
printKV(userPlan);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log(` ${chalk.dim('Plan'.padEnd(16))} : ${chalk.bold('Free')}`);
|
|
181
|
+
console.log(` ${chalk.dim('Status'.padEnd(16))} : ${chalk.green('Active')}`);
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(chalk.dim(' Upgrade with: ') + chalk.cyan('minara premium subscribe'));
|
|
184
|
+
}
|
|
185
|
+
console.log('');
|
|
186
|
+
}));
|
|
187
|
+
// ─── subscribe ──────────────────────────────────────────────────────────
|
|
188
|
+
const subscribeCmd = new Command('subscribe')
|
|
189
|
+
.description('Subscribe to a plan or change your plan (upgrade / downgrade)')
|
|
190
|
+
.action(wrapAction(async () => {
|
|
191
|
+
const creds = requireAuth();
|
|
192
|
+
// 1. Fetch plans
|
|
193
|
+
const spin = spinner('Fetching plans…');
|
|
194
|
+
const plansRes = await paymentApi.getPlans();
|
|
195
|
+
spin.stop();
|
|
196
|
+
assertApiOk(plansRes, 'Failed to fetch plans');
|
|
197
|
+
const { plans } = plansRes.data;
|
|
198
|
+
const activePlans = plans.filter((p) => p.status === 'active' && p.price > 0);
|
|
199
|
+
if (activePlans.length === 0) {
|
|
200
|
+
info('No paid plans available.');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// 2. Select plan
|
|
204
|
+
const selectedPlanId = await select({
|
|
205
|
+
message: 'Select a plan:',
|
|
206
|
+
choices: activePlans.map((p) => ({
|
|
207
|
+
name: `${p.name} (${p.interval === 'month' ? 'Monthly' : 'Yearly'}) — ${formatPrice(p)} [${formatCredits(p.rules)} credits, ${p.rules.limitWorkflows ?? 0} workflows]`,
|
|
208
|
+
value: p._id,
|
|
209
|
+
})),
|
|
210
|
+
});
|
|
211
|
+
const selectedPlan = activePlans.find((p) => p._id === selectedPlanId);
|
|
212
|
+
// 3. Select payment method
|
|
213
|
+
const payMethod = await select({
|
|
214
|
+
message: 'Payment method:',
|
|
215
|
+
choices: [
|
|
216
|
+
{ name: 'Credit Card (Stripe)', value: 'stripe' },
|
|
217
|
+
{ name: 'Crypto (USDC on-chain)', value: 'crypto' },
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
// 4. Confirm
|
|
221
|
+
const priceStr = `$${selectedPlan.price}/${selectedPlan.interval === 'month' ? 'mo' : 'yr'}`;
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(chalk.bold('Order Summary:'));
|
|
224
|
+
console.log(` Plan : ${chalk.bold(selectedPlan.name)} (${selectedPlan.interval === 'month' ? 'Monthly' : 'Yearly'})`);
|
|
225
|
+
console.log(` Price : ${chalk.bold(priceStr)}`);
|
|
226
|
+
console.log(` Payment : ${payMethod === 'stripe' ? 'Credit Card (Stripe)' : 'Crypto (USDC)'}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
const ok = await confirm({ message: 'Proceed to checkout?', default: true });
|
|
229
|
+
if (!ok) {
|
|
230
|
+
console.log(chalk.dim('Cancelled.'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// 5. Create checkout
|
|
234
|
+
if (payMethod === 'stripe') {
|
|
235
|
+
await handleStripeCheckout(creds.accessToken, selectedPlanId);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
await handleCryptoCheckout(creds.accessToken, selectedPlanId);
|
|
239
|
+
}
|
|
240
|
+
}));
|
|
241
|
+
async function handleStripeCheckout(token, planId) {
|
|
242
|
+
const spin = spinner('Creating checkout session…');
|
|
243
|
+
const res = await paymentApi.checkoutPlan(token, planId, 'https://minara.ai/payment/success', 'https://minara.ai/payment/cancel');
|
|
244
|
+
spin.stop();
|
|
245
|
+
assertApiOk(res, 'Failed to create checkout session');
|
|
246
|
+
const data = res.data;
|
|
247
|
+
const url = data.url ?? data.checkoutUrl;
|
|
248
|
+
if (url) {
|
|
249
|
+
success('Checkout session created!');
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log(chalk.dim(' Opening browser for payment…'));
|
|
252
|
+
console.log(chalk.cyan(` ${url}`));
|
|
253
|
+
console.log('');
|
|
254
|
+
openBrowser(url);
|
|
255
|
+
info('Complete the payment in your browser. Your subscription will activate automatically.');
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Fallback: show all returned data
|
|
259
|
+
success('Checkout session created:');
|
|
260
|
+
printKV(data);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function handleCryptoCheckout(token, planId) {
|
|
264
|
+
const spin = spinner('Creating crypto checkout…');
|
|
265
|
+
const res = await paymentApi.cryptoCheckoutPlan(token, planId);
|
|
266
|
+
spin.stop();
|
|
267
|
+
assertApiOk(res, 'Failed to create crypto checkout');
|
|
268
|
+
const data = res.data;
|
|
269
|
+
const url = data.url ?? data.checkoutUrl;
|
|
270
|
+
if (url) {
|
|
271
|
+
success('Crypto checkout created!');
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(chalk.dim(' Opening browser for crypto payment…'));
|
|
274
|
+
console.log(chalk.cyan(` ${url}`));
|
|
275
|
+
console.log('');
|
|
276
|
+
openBrowser(url);
|
|
277
|
+
info('Complete the crypto payment in your browser. Your subscription will activate after confirmation.');
|
|
278
|
+
}
|
|
279
|
+
else if (data.address) {
|
|
280
|
+
success('Crypto payment details:');
|
|
281
|
+
console.log('');
|
|
282
|
+
printKV(data);
|
|
283
|
+
console.log('');
|
|
284
|
+
info('Send the exact amount to the address above. Your subscription will activate after on-chain confirmation.');
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
success('Crypto checkout created:');
|
|
288
|
+
printKV(data);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ─── buy-credits ────────────────────────────────────────────────────────
|
|
292
|
+
const buyCreditsCmd = new Command('buy-credits')
|
|
293
|
+
.description('Buy a one-time credit package')
|
|
294
|
+
.action(wrapAction(async () => {
|
|
295
|
+
const creds = requireAuth();
|
|
296
|
+
const spin = spinner('Fetching packages…');
|
|
297
|
+
const plansRes = await paymentApi.getPlans();
|
|
298
|
+
spin.stop();
|
|
299
|
+
assertApiOk(plansRes, 'Failed to fetch packages');
|
|
300
|
+
const { packages } = plansRes.data;
|
|
301
|
+
if (packages.length === 0) {
|
|
302
|
+
info('No credit packages available.');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Select package
|
|
306
|
+
const selectedPkgId = await select({
|
|
307
|
+
message: 'Select a credit package:',
|
|
308
|
+
choices: packages.map((pkg) => ({
|
|
309
|
+
name: `$${pkg.amount} — ${Number(pkg.credit).toLocaleString()} credits`,
|
|
310
|
+
value: pkg._id,
|
|
311
|
+
})),
|
|
312
|
+
});
|
|
313
|
+
const selectedPkg = packages.find((p) => p._id === selectedPkgId);
|
|
314
|
+
// Select payment method
|
|
315
|
+
const payMethod = await select({
|
|
316
|
+
message: 'Payment method:',
|
|
317
|
+
choices: [
|
|
318
|
+
{ name: 'Credit Card (Stripe)', value: 'stripe' },
|
|
319
|
+
{ name: 'Crypto (USDC on-chain)', value: 'crypto' },
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log(chalk.bold('Package Summary:'));
|
|
324
|
+
console.log(` Price : ${chalk.bold('$' + selectedPkg.amount)}`);
|
|
325
|
+
console.log(` Credits : ${Number(selectedPkg.credit).toLocaleString()}`);
|
|
326
|
+
console.log(` Payment : ${payMethod === 'stripe' ? 'Credit Card (Stripe)' : 'Crypto (USDC)'}`);
|
|
327
|
+
console.log('');
|
|
328
|
+
const ok = await confirm({ message: 'Proceed to checkout?', default: true });
|
|
329
|
+
if (!ok) {
|
|
330
|
+
console.log(chalk.dim('Cancelled.'));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (payMethod === 'stripe') {
|
|
334
|
+
const spin2 = spinner('Creating checkout session…');
|
|
335
|
+
const res = await paymentApi.checkoutPackage(creds.accessToken, selectedPkgId, 'https://minara.ai/payment/success', 'https://minara.ai/payment/cancel');
|
|
336
|
+
spin2.stop();
|
|
337
|
+
assertApiOk(res, 'Failed to create checkout');
|
|
338
|
+
const url = res.data?.url ?? res.data?.checkoutUrl;
|
|
339
|
+
if (url) {
|
|
340
|
+
success('Opening browser for payment…');
|
|
341
|
+
console.log(chalk.cyan(` ${url}`));
|
|
342
|
+
openBrowser(url);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
success('Checkout created:');
|
|
346
|
+
printKV(res.data);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const spin2 = spinner('Creating crypto checkout…');
|
|
351
|
+
const res = await paymentApi.cryptoCheckoutPackage(creds.accessToken, selectedPkgId);
|
|
352
|
+
spin2.stop();
|
|
353
|
+
assertApiOk(res, 'Failed to create crypto checkout');
|
|
354
|
+
const url = res.data?.url ?? res.data?.checkoutUrl;
|
|
355
|
+
if (url) {
|
|
356
|
+
success('Opening browser for crypto payment…');
|
|
357
|
+
console.log(chalk.cyan(` ${url}`));
|
|
358
|
+
openBrowser(url);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
success('Crypto checkout:');
|
|
362
|
+
printKV(res.data);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
// ─── cancel ─────────────────────────────────────────────────────────────
|
|
367
|
+
const cancelCmd = new Command('cancel')
|
|
368
|
+
.description('Cancel your current subscription')
|
|
369
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
370
|
+
.action(wrapAction(async (opts) => {
|
|
371
|
+
const creds = requireAuth();
|
|
372
|
+
if (!opts.yes) {
|
|
373
|
+
warn('Cancelling your subscription will downgrade you to the Free plan at the end of your billing period.');
|
|
374
|
+
console.log('');
|
|
375
|
+
const ok = await confirm({
|
|
376
|
+
message: 'Are you sure you want to cancel your subscription?',
|
|
377
|
+
default: false,
|
|
378
|
+
});
|
|
379
|
+
if (!ok) {
|
|
380
|
+
console.log(chalk.dim('Kept your subscription.'));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const spin = spinner('Cancelling subscription…');
|
|
385
|
+
const res = await paymentApi.cancelSubscription(creds.accessToken);
|
|
386
|
+
spin.stop();
|
|
387
|
+
assertApiOk(res, 'Failed to cancel subscription');
|
|
388
|
+
success('Subscription cancelled.');
|
|
389
|
+
info('You will continue to have access until the end of your current billing period.');
|
|
390
|
+
if (res.data && typeof res.data === 'object' && Object.keys(res.data).length > 0) {
|
|
391
|
+
console.log('');
|
|
392
|
+
printKV(res.data);
|
|
393
|
+
}
|
|
394
|
+
}));
|
|
395
|
+
// ─── parent ─────────────────────────────────────────────────────────────
|
|
396
|
+
export const premiumCommand = new Command('premium')
|
|
397
|
+
.description('Manage your Minara subscription — plans, subscribe, cancel')
|
|
398
|
+
.addCommand(plansCmd)
|
|
399
|
+
.addCommand(statusCmd)
|
|
400
|
+
.addCommand(subscribeCmd)
|
|
401
|
+
.addCommand(buyCreditsCmd)
|
|
402
|
+
.addCommand(cancelCmd)
|
|
403
|
+
.action(wrapAction(async () => {
|
|
404
|
+
const action = await select({
|
|
405
|
+
message: 'Premium:',
|
|
406
|
+
choices: [
|
|
407
|
+
{ name: 'View available plans', value: 'plans' },
|
|
408
|
+
{ name: 'View my subscription', value: 'status' },
|
|
409
|
+
{ name: 'Subscribe / Change plan', value: 'subscribe' },
|
|
410
|
+
{ name: 'Buy credit package', value: 'buy-credits' },
|
|
411
|
+
{ name: 'Cancel subscription', value: 'cancel' },
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
const sub = premiumCommand.commands.find((c) => c.name() === action);
|
|
415
|
+
if (sub)
|
|
416
|
+
await sub.parseAsync([], { from: 'user' });
|
|
417
|
+
}));
|
package/dist/commands/swap.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { input, select, confirm } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
4
|
+
import { swaps, swapsSimulate } from '../api/crosschain.js';
|
|
5
|
+
import { get } from '../api/client.js';
|
|
5
6
|
import { requireAuth } from '../config.js';
|
|
6
|
-
import { success, info, spinner, formatOrderSide, assertApiOk,
|
|
7
|
+
import { success, info, warn, spinner, formatOrderSide, assertApiOk, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel, normalizeChain } from '../utils.js';
|
|
8
|
+
import { requireTouchId } from '../touchid.js';
|
|
9
|
+
import { printTxResult, printKV } from '../formatters.js';
|
|
7
10
|
export const swapCommand = new Command('swap')
|
|
8
11
|
.description('Swap tokens (cross-chain spot trading)')
|
|
9
|
-
.option('-c, --chain <chain>', 'Blockchain (e.g. solana, base, ethereum)')
|
|
10
12
|
.option('-s, --side <side>', 'buy or sell')
|
|
11
|
-
.option('-t, --token <address>', 'Token contract address')
|
|
13
|
+
.option('-t, --token <address|ticker>', 'Token contract address or ticker symbol')
|
|
12
14
|
.option('-a, --amount <amount>', 'USD amount (buy) or token amount (sell)')
|
|
13
15
|
.option('-y, --yes', 'Skip confirmation')
|
|
14
16
|
.option('--dry-run', 'Simulate without executing')
|
|
15
17
|
.action(wrapAction(async (opts) => {
|
|
16
18
|
const creds = requireAuth();
|
|
17
|
-
// ── 1.
|
|
18
|
-
const chain = opts.chain ?? await selectChain();
|
|
19
|
-
// ── 2. Side ──────────────────────────────────────────────────────────
|
|
19
|
+
// ── 1. Side ──────────────────────────────────────────────────────────
|
|
20
20
|
let side = opts.side;
|
|
21
21
|
if (!side) {
|
|
22
22
|
side = await select({
|
|
@@ -27,26 +27,66 @@ export const swapCommand = new Command('swap')
|
|
|
27
27
|
],
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
// ──
|
|
31
|
-
const
|
|
32
|
-
message: 'Token contract address:',
|
|
33
|
-
validate: (v) => (v.length >
|
|
30
|
+
// ── 2. Token ───────────────────────────────────────────────────────────
|
|
31
|
+
const tokenInput = opts.token ?? await input({
|
|
32
|
+
message: 'Token (contract address or ticker):',
|
|
33
|
+
validate: (v) => (v.length > 0 ? true : 'Please enter a token address or ticker'),
|
|
34
34
|
});
|
|
35
|
+
const tokenInfo = await lookupToken(tokenInput);
|
|
36
|
+
// ── 3. Chain (derived from token) ────────────────────────────────────
|
|
37
|
+
const chain = normalizeChain(tokenInfo.chain);
|
|
38
|
+
if (!chain) {
|
|
39
|
+
warn(`Unable to determine chain for token. Raw chain value: ${tokenInfo.chain ?? 'unknown'}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
35
42
|
// ── 4. Amount ────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
let maxBalance;
|
|
44
|
+
if (side === 'sell') {
|
|
45
|
+
const pnlRes = await get('/users/pnls/all', { token: creds.accessToken });
|
|
46
|
+
if (pnlRes.success && Array.isArray(pnlRes.data)) {
|
|
47
|
+
const match = pnlRes.data.find((t) => {
|
|
48
|
+
const addr = String(t.tokenAddress ?? '').toLowerCase();
|
|
49
|
+
const cid = String(t.chainId ?? '').toLowerCase();
|
|
50
|
+
const targetChain = chain.toLowerCase();
|
|
51
|
+
return addr === tokenInfo.address.toLowerCase() && cid === targetChain;
|
|
52
|
+
});
|
|
53
|
+
if (match) {
|
|
54
|
+
maxBalance = Number(match.balance ?? 0);
|
|
55
|
+
info(`Available balance: ${chalk.bold(String(maxBalance))} ${tokenInfo.symbol ?? ''}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const amountLabel = side === 'buy' ? 'USD amount to spend' : `Token amount to sell${maxBalance ? ' ("all" for max)' : ''}`;
|
|
60
|
+
let amount = opts.amount ?? await input({
|
|
38
61
|
message: `${amountLabel}:`,
|
|
39
62
|
validate: (v) => {
|
|
63
|
+
if (side === 'sell' && v.toLowerCase() === 'all')
|
|
64
|
+
return true;
|
|
40
65
|
const n = parseFloat(v);
|
|
41
66
|
return (isNaN(n) || n <= 0) ? 'Enter a valid positive number' : true;
|
|
42
67
|
},
|
|
43
68
|
});
|
|
69
|
+
if (side === 'sell') {
|
|
70
|
+
if (amount.toLowerCase() === 'all') {
|
|
71
|
+
if (!maxBalance || maxBalance <= 0) {
|
|
72
|
+
warn('Could not determine balance. Please enter an amount manually.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
amount = String(maxBalance);
|
|
76
|
+
info(`Selling all: ${chalk.bold(amount)} ${tokenInfo.symbol ?? ''}`);
|
|
77
|
+
}
|
|
78
|
+
else if (maxBalance && parseFloat(amount) > maxBalance) {
|
|
79
|
+
info(`Amount exceeds balance (${maxBalance}), using max balance`);
|
|
80
|
+
amount = String(maxBalance);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
44
83
|
// ── 5. Summary ───────────────────────────────────────────────────────
|
|
45
84
|
console.log('');
|
|
46
85
|
console.log(chalk.bold('Swap Summary:'));
|
|
47
86
|
console.log(` Chain : ${chalk.cyan(chain)}`);
|
|
48
87
|
console.log(` Action : ${formatOrderSide(side)}`);
|
|
49
|
-
console.log(` Token : ${
|
|
88
|
+
console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
|
|
89
|
+
console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
|
|
50
90
|
console.log(` Amount : ${chalk.bold(amount)} ${side === 'buy' ? 'USD' : '(token)'}`);
|
|
51
91
|
console.log('');
|
|
52
92
|
// ── 6. Dry run ───────────────────────────────────────────────────────
|
|
@@ -54,11 +94,21 @@ export const swapCommand = new Command('swap')
|
|
|
54
94
|
info('Simulating swap (dry-run)…');
|
|
55
95
|
const spin = spinner('Simulating…');
|
|
56
96
|
const simRes = await swapsSimulate(creds.accessToken, [{
|
|
57
|
-
chain, side, tokenAddress, buyUsdAmountOrSellTokenAmount: amount,
|
|
97
|
+
chain, side, tokenAddress: tokenInfo.address, buyUsdAmountOrSellTokenAmount: amount,
|
|
58
98
|
}]);
|
|
59
99
|
spin.stop();
|
|
60
100
|
assertApiOk(simRes, 'Simulation failed');
|
|
61
|
-
console.log(
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(chalk.bold('Simulation Result:'));
|
|
103
|
+
if (Array.isArray(simRes.data)) {
|
|
104
|
+
for (const item of simRes.data) {
|
|
105
|
+
printKV(item);
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (simRes.data) {
|
|
110
|
+
printKV(simRes.data);
|
|
111
|
+
}
|
|
62
112
|
return;
|
|
63
113
|
}
|
|
64
114
|
// ── 7. Confirm ───────────────────────────────────────────────────────
|
|
@@ -72,14 +122,22 @@ export const swapCommand = new Command('swap')
|
|
|
72
122
|
return;
|
|
73
123
|
}
|
|
74
124
|
}
|
|
75
|
-
// ── 8.
|
|
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
|
+
await requireTouchId();
|
|
128
|
+
// ── 9. Execute ───────────────────────────────────────────────────────
|
|
76
129
|
const spin = spinner('Executing swap…');
|
|
77
|
-
const res = await
|
|
78
|
-
|
|
79
|
-
|
|
130
|
+
const res = await swaps(creds.accessToken, [{
|
|
131
|
+
chain, side, tokenAddress: tokenInfo.address, buyUsdAmountOrSellTokenAmount: amount,
|
|
132
|
+
}]);
|
|
80
133
|
spin.stop();
|
|
81
134
|
assertApiOk(res, 'Swap failed');
|
|
82
135
|
success('Swap submitted!');
|
|
83
|
-
if (res.data)
|
|
84
|
-
|
|
136
|
+
if (Array.isArray(res.data)) {
|
|
137
|
+
for (const tx of res.data)
|
|
138
|
+
printTxResult(tx);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
printTxResult(res.data);
|
|
142
|
+
}
|
|
85
143
|
}));
|
|
@@ -3,11 +3,13 @@ import { input, confirm } from '@inquirer/prompts';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { transfer } from '../api/crosschain.js';
|
|
5
5
|
import { requireAuth } from '../config.js';
|
|
6
|
-
import { success, warn, spinner, assertApiOk, selectChain, wrapAction } from '../utils.js';
|
|
6
|
+
import { success, warn, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
|
|
7
|
+
import { requireTouchId } from '../touchid.js';
|
|
8
|
+
import { printTxResult } from '../formatters.js';
|
|
7
9
|
export const transferCommand = new Command('transfer')
|
|
8
10
|
.description('Transfer tokens to another address')
|
|
9
11
|
.option('-c, --chain <chain>', 'Blockchain')
|
|
10
|
-
.option('-t, --token <address>', 'Token contract address')
|
|
12
|
+
.option('-t, --token <address|ticker>', 'Token contract address or ticker symbol')
|
|
11
13
|
.option('-a, --amount <amount>', 'Token amount to send')
|
|
12
14
|
.option('--to <address>', 'Recipient address')
|
|
13
15
|
.option('-y, --yes', 'Skip confirmation')
|
|
@@ -16,10 +18,11 @@ export const transferCommand = new Command('transfer')
|
|
|
16
18
|
// ── 1. Chain ─────────────────────────────────────────────────────────
|
|
17
19
|
const chain = opts.chain ?? await selectChain();
|
|
18
20
|
// ── 2. Token ─────────────────────────────────────────────────────────
|
|
19
|
-
const
|
|
20
|
-
message: 'Token
|
|
21
|
-
validate: (v) => (v.length > 0 ? true : '
|
|
21
|
+
const tokenInput = opts.token ?? await input({
|
|
22
|
+
message: 'Token (address or ticker, native = 0x0…0):',
|
|
23
|
+
validate: (v) => (v.length > 0 ? true : 'Token is required'),
|
|
22
24
|
});
|
|
25
|
+
const tokenInfo = await lookupToken(tokenInput);
|
|
23
26
|
// ── 3. Amount ────────────────────────────────────────────────────────
|
|
24
27
|
const amount = opts.amount ?? await input({
|
|
25
28
|
message: 'Amount to send:',
|
|
@@ -33,11 +36,12 @@ export const transferCommand = new Command('transfer')
|
|
|
33
36
|
message: 'Recipient address:',
|
|
34
37
|
validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
|
|
35
38
|
});
|
|
36
|
-
// ── 5. Summary
|
|
39
|
+
// ── 5. Summary ───────────────────────────────────────────────────────
|
|
37
40
|
console.log('');
|
|
38
41
|
console.log(chalk.bold.red('⚠ Transfer Summary:'));
|
|
39
42
|
console.log(` Chain : ${chalk.cyan(chain)}`);
|
|
40
|
-
console.log(` Token : ${
|
|
43
|
+
console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
|
|
44
|
+
console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
|
|
41
45
|
console.log(` Amount : ${chalk.bold(amount)}`);
|
|
42
46
|
console.log(` To : ${chalk.yellow(recipient)}`);
|
|
43
47
|
console.log('');
|
|
@@ -49,12 +53,14 @@ export const transferCommand = new Command('transfer')
|
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
// ── 6.
|
|
56
|
+
// ── 6. Transaction confirmation & Touch ID ────────────────────────────
|
|
57
|
+
await requireTransactionConfirmation(`Transfer ${amount} tokens → ${recipient} · ${chain}`, tokenInfo, { chain, amount });
|
|
58
|
+
await requireTouchId();
|
|
59
|
+
// ── 7. Execute ───────────────────────────────────────────────────────
|
|
53
60
|
const spin = spinner('Processing transfer…');
|
|
54
|
-
const res = await transfer(creds.accessToken, { chain, tokenAddress, tokenAmount: amount, recipient });
|
|
61
|
+
const res = await transfer(creds.accessToken, { chain, tokenAddress: tokenInfo.address, tokenAmount: amount, recipient });
|
|
55
62
|
spin.stop();
|
|
56
63
|
assertApiOk(res, 'Transfer failed');
|
|
57
64
|
success('Transfer submitted!');
|
|
58
|
-
|
|
59
|
-
console.log(JSON.stringify(res.data, null, 2));
|
|
65
|
+
printTxResult(res.data);
|
|
60
66
|
}));
|