|
1 | 1 | import chalk from 'chalk'; |
| 2 | +import open from 'open'; |
2 | 3 | import { getConnection } from '../config.js'; |
3 | 4 |
|
4 | | -const ACCOUNT_URL = 'https://accounts.atxp.ai/me'; |
| 5 | +const DEFAULT_AMOUNT = 10; |
| 6 | +const MIN_AMOUNT = 1; |
| 7 | +const MAX_AMOUNT = 1000; |
5 | 8 |
|
6 | | -/** |
7 | | - * Extract the connection_token from the ATXP_CONNECTION string. |
8 | | - * Connection string format: https://accounts.atxp.ai?connection_token=<token> |
9 | | - */ |
10 | | -function getConnectionToken(connectionString: string): string | null { |
11 | | - try { |
12 | | - const url = new URL(connectionString); |
13 | | - return url.searchParams.get('connection_token'); |
14 | | - } catch { |
15 | | - return null; |
16 | | - } |
17 | | -} |
18 | | - |
19 | | -export async function depositCommand(): Promise<void> { |
| 9 | +function getAccountsAuth(): { baseUrl: string; token: string } { |
20 | 10 | const connection = getConnection(); |
21 | | - |
22 | 11 | if (!connection) { |
23 | 12 | console.error(chalk.red('Not logged in.')); |
24 | 13 | console.error(`Run: ${chalk.cyan('npx atxp login')}`); |
25 | 14 | process.exit(1); |
26 | 15 | } |
27 | | - |
28 | | - const token = getConnectionToken(connection); |
| 16 | + const url = new URL(connection); |
| 17 | + const token = url.searchParams.get('connection_token'); |
29 | 18 | if (!token) { |
30 | | - console.error(chalk.red('Error: Could not extract connection token.')); |
31 | | - console.error('Your ATXP_CONNECTION may be malformed. Try logging in again:'); |
32 | | - console.error(chalk.cyan(' npx atxp login --force')); |
| 19 | + console.error(chalk.red('Invalid connection string: missing connection_token')); |
33 | 20 | process.exit(1); |
34 | 21 | } |
| 22 | + return { baseUrl: `${url.protocol}//${url.host}`, token }; |
| 23 | +} |
| 24 | + |
| 25 | +function getArgValue(flag: string): string | undefined { |
| 26 | + const index = process.argv.findIndex((arg) => arg === flag); |
| 27 | + return index !== -1 ? process.argv[index + 1] : undefined; |
| 28 | +} |
| 29 | + |
| 30 | +function showFundHelp(): void { |
| 31 | + console.log(chalk.bold('Fund Commands:')); |
| 32 | + console.log(); |
| 33 | + console.log(' ' + chalk.cyan('npx atxp fund') + ' ' + 'Show funding options for your account'); |
| 34 | + console.log(' ' + chalk.cyan('npx atxp fund --amount 100') + ' ' + 'Request a $100 payment link (agents only)'); |
| 35 | + console.log(); |
| 36 | + console.log(chalk.bold('Options:')); |
| 37 | + console.log(' ' + chalk.yellow('--amount') + ' ' + `Suggested amount in USD ($${MIN_AMOUNT}-$${MAX_AMOUNT}, default: $${DEFAULT_AMOUNT})`); |
| 38 | + console.log(' ' + chalk.yellow('--open') + ' ' + 'Open the payment link in your browser'); |
| 39 | + console.log(); |
| 40 | + console.log(chalk.bold('How it works:')); |
| 41 | + console.log(' Shows all available ways to fund your account:'); |
| 42 | + console.log(' - Crypto deposit addresses (USDC on supported chains)'); |
| 43 | + console.log(' - Payment link (agent accounts only) - shareable Stripe link'); |
| 44 | + console.log(' The payer can adjust the amount at checkout.'); |
| 45 | +} |
| 46 | + |
| 47 | +export async function depositCommand(): Promise<void> { |
| 48 | + if (process.argv.includes('--help') || process.argv.includes('-h')) { |
| 49 | + showFundHelp(); |
| 50 | + return; |
| 51 | + } |
| 52 | + |
| 53 | + const { baseUrl, token } = getAccountsAuth(); |
| 54 | + |
| 55 | + // Parse amount for payment link |
| 56 | + const amountStr = getArgValue('--amount'); |
| 57 | + let amount = DEFAULT_AMOUNT; |
| 58 | + if (amountStr) { |
| 59 | + amount = parseFloat(amountStr); |
| 60 | + if (isNaN(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) { |
| 61 | + console.error(chalk.red(`Invalid amount: must be between $${MIN_AMOUNT} and $${MAX_AMOUNT}`)); |
| 62 | + process.exit(1); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + const shouldOpen = process.argv.includes('--open'); |
| 67 | + |
| 68 | + console.log(chalk.gray('Fetching funding options...')); |
35 | 69 |
|
36 | 70 | try { |
37 | | - const credentials = Buffer.from(`${token}:`).toString('base64'); |
38 | | - const response = await fetch(ACCOUNT_URL, { |
| 71 | + const res = await fetch(`${baseUrl}/api/funding/fund`, { |
| 72 | + method: 'POST', |
39 | 73 | headers: { |
40 | | - 'Authorization': `Basic ${credentials}`, |
| 74 | + 'Authorization': `Bearer ${token}`, |
41 | 75 | 'Content-Type': 'application/json', |
42 | 76 | }, |
| 77 | + body: JSON.stringify({ amount }), |
43 | 78 | }); |
44 | 79 |
|
45 | | - if (!response.ok) { |
46 | | - console.error(chalk.red(`Error: ${response.status} ${response.statusText}`)); |
| 80 | + if (!res.ok) { |
| 81 | + const body = await res.json().catch(() => ({})) as Record<string, string>; |
| 82 | + console.error(chalk.red(`Error: ${body.error || res.statusText}`)); |
47 | 83 | process.exit(1); |
48 | 84 | } |
49 | 85 |
|
50 | | - const data = await response.json(); |
| 86 | + const data = await res.json() as { |
| 87 | + paymentLink: { |
| 88 | + url: string; |
| 89 | + paymentLinkId: string; |
| 90 | + suggestedAmount: number; |
| 91 | + minAmount: number; |
| 92 | + maxAmount: number; |
| 93 | + } | null; |
| 94 | + cryptoDeposit: Array<{ |
| 95 | + address: string; |
| 96 | + network: string; |
| 97 | + currency: string; |
| 98 | + }>; |
| 99 | + }; |
51 | 100 |
|
52 | | - if (data.sources && Array.isArray(data.sources) && data.sources.length > 0) { |
| 101 | + console.log(); |
| 102 | + |
| 103 | + // Display crypto deposit addresses |
| 104 | + if (data.cryptoDeposit && data.cryptoDeposit.length > 0) { |
53 | 105 | console.log(chalk.bold('Fund via USDC:')); |
54 | | - for (const source of data.sources) { |
55 | | - const chain = source.chain.charAt(0).toUpperCase() + source.chain.slice(1); |
| 106 | + for (const source of data.cryptoDeposit) { |
| 107 | + const chain = source.network.charAt(0).toUpperCase() + source.network.slice(1); |
56 | 108 | console.log(` ${chalk.cyan(chain)}: ${source.address}`); |
57 | 109 | } |
58 | 110 | console.log(); |
59 | | - console.log(chalk.bold('Fund via credit card or other payment methods:')); |
60 | | - console.log(` ${chalk.underline('https://accounts.atxp.ai/fund')}`); |
61 | | - } else { |
62 | | - console.log(chalk.yellow('No deposit addresses found for this account.')); |
| 111 | + } |
| 112 | + |
| 113 | + // Display payment link (agent accounts only) |
| 114 | + if (data.paymentLink) { |
| 115 | + console.log(chalk.bold('Fund via payment link:')); |
| 116 | + console.log(' ' + chalk.bold('Suggested:') + ' ' + chalk.green(`$${data.paymentLink.suggestedAmount.toFixed(2)}`)); |
| 117 | + console.log(' ' + chalk.bold('Range:') + ' ' + chalk.gray(`$${data.paymentLink.minAmount} - $${data.paymentLink.maxAmount}`)); |
| 118 | + console.log(' ' + chalk.bold('URL:') + ' ' + chalk.cyan.underline(data.paymentLink.url)); |
| 119 | + console.log(); |
| 120 | + console.log(chalk.gray('Share this link with anyone to fund your account.')); |
| 121 | + console.log(chalk.gray('The payer can adjust the amount at checkout.')); |
| 122 | + |
| 123 | + if (shouldOpen) { |
| 124 | + console.log(); |
| 125 | + console.log(chalk.gray('Opening in browser...')); |
| 126 | + await open(data.paymentLink.url); |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + // If neither is available |
| 131 | + if ((!data.cryptoDeposit || data.cryptoDeposit.length === 0) && !data.paymentLink) { |
| 132 | + console.log(chalk.yellow('No funding options found for this account.')); |
63 | 133 | console.log(); |
64 | 134 | console.log(chalk.bold('Fund via credit card or other payment methods:')); |
65 | 135 | console.log(` ${chalk.underline('https://accounts.atxp.ai/fund')}`); |
66 | 136 | } |
67 | 137 | } catch (error) { |
68 | 138 | const errorMessage = error instanceof Error ? error.message : String(error); |
69 | | - console.error(chalk.red(`Error fetching funding info: ${errorMessage}`)); |
| 139 | + console.error(chalk.red(`Error fetching funding options: ${errorMessage}`)); |
70 | 140 | process.exit(1); |
71 | 141 | } |
72 | 142 | } |
0 commit comments