paytaca-cli 0.6.1 → 0.7.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.
Files changed (130) hide show
  1. package/README.md +153 -37
  2. package/dist/ai/autoRefill.d.ts +27 -0
  3. package/dist/ai/autoRefill.d.ts.map +1 -0
  4. package/dist/ai/autoRefill.js +75 -0
  5. package/dist/ai/autoRefill.js.map +1 -0
  6. package/dist/ai/client.d.ts +73 -0
  7. package/dist/ai/client.d.ts.map +1 -0
  8. package/dist/ai/client.js +90 -0
  9. package/dist/ai/client.js.map +1 -0
  10. package/dist/ai/config.d.ts +7 -0
  11. package/dist/ai/config.d.ts.map +1 -0
  12. package/dist/ai/config.js +16 -0
  13. package/dist/ai/config.js.map +1 -0
  14. package/dist/ai/configure.d.ts +61 -0
  15. package/dist/ai/configure.d.ts.map +1 -0
  16. package/dist/ai/configure.js +452 -0
  17. package/dist/ai/configure.js.map +1 -0
  18. package/dist/ai/credits.d.ts +27 -0
  19. package/dist/ai/credits.d.ts.map +1 -0
  20. package/dist/ai/credits.js +99 -0
  21. package/dist/ai/credits.js.map +1 -0
  22. package/dist/ai/models.d.ts +13 -0
  23. package/dist/ai/models.d.ts.map +1 -0
  24. package/dist/ai/models.js +49 -0
  25. package/dist/ai/models.js.map +1 -0
  26. package/dist/ai/oauth.d.ts +45 -0
  27. package/dist/ai/oauth.d.ts.map +1 -0
  28. package/dist/ai/oauth.js +137 -0
  29. package/dist/ai/oauth.js.map +1 -0
  30. package/dist/ai/purchase.d.ts +30 -0
  31. package/dist/ai/purchase.d.ts.map +1 -0
  32. package/dist/ai/purchase.js +456 -0
  33. package/dist/ai/purchase.js.map +1 -0
  34. package/dist/commands/address.d.ts.map +1 -1
  35. package/dist/commands/address.js +88 -36
  36. package/dist/commands/address.js.map +1 -1
  37. package/dist/commands/ai.d.ts +9 -0
  38. package/dist/commands/ai.d.ts.map +1 -0
  39. package/dist/commands/ai.js +675 -0
  40. package/dist/commands/ai.js.map +1 -0
  41. package/dist/commands/balance.d.ts.map +1 -1
  42. package/dist/commands/balance.js +68 -129
  43. package/dist/commands/balance.js.map +1 -1
  44. package/dist/commands/history.d.ts.map +1 -1
  45. package/dist/commands/history.js +38 -78
  46. package/dist/commands/history.js.map +1 -1
  47. package/dist/commands/mcp.d.ts +14 -0
  48. package/dist/commands/mcp.d.ts.map +1 -0
  49. package/dist/commands/mcp.js +68 -0
  50. package/dist/commands/mcp.js.map +1 -0
  51. package/dist/commands/pay.js +1 -1
  52. package/dist/commands/pay.js.map +1 -1
  53. package/dist/commands/receive.d.ts +1 -8
  54. package/dist/commands/receive.d.ts.map +1 -1
  55. package/dist/commands/receive.js +51 -124
  56. package/dist/commands/receive.js.map +1 -1
  57. package/dist/commands/send.d.ts +0 -10
  58. package/dist/commands/send.d.ts.map +1 -1
  59. package/dist/commands/send.js +40 -50
  60. package/dist/commands/send.js.map +1 -1
  61. package/dist/commands/swap.js +1 -1
  62. package/dist/commands/swap.js.map +1 -1
  63. package/dist/commands/token.d.ts +1 -9
  64. package/dist/commands/token.d.ts.map +1 -1
  65. package/dist/commands/token.js +192 -289
  66. package/dist/commands/token.js.map +1 -1
  67. package/dist/core/context.d.ts +30 -0
  68. package/dist/core/context.d.ts.map +1 -0
  69. package/dist/core/context.js +45 -0
  70. package/dist/core/context.js.map +1 -0
  71. package/dist/core/wallet.d.ts +130 -0
  72. package/dist/core/wallet.d.ts.map +1 -0
  73. package/dist/core/wallet.js +276 -0
  74. package/dist/core/wallet.js.map +1 -0
  75. package/dist/index.js +4 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/mcp/server.d.ts +8 -0
  78. package/dist/mcp/server.d.ts.map +1 -0
  79. package/dist/mcp/server.js +24 -0
  80. package/dist/mcp/server.js.map +1 -0
  81. package/dist/mcp/tools.d.ts +5 -0
  82. package/dist/mcp/tools.d.ts.map +1 -0
  83. package/dist/mcp/tools.js +473 -0
  84. package/dist/mcp/tools.js.map +1 -0
  85. package/dist/utils/format.d.ts +23 -0
  86. package/dist/utils/format.d.ts.map +1 -0
  87. package/dist/utils/format.js +62 -0
  88. package/dist/utils/format.js.map +1 -0
  89. package/dist/utils/x402.js +1 -1
  90. package/dist/utils/x402.js.map +1 -1
  91. package/dist/wallet/index.d.ts +13 -0
  92. package/dist/wallet/index.d.ts.map +1 -1
  93. package/dist/wallet/index.js +18 -0
  94. package/dist/wallet/index.js.map +1 -1
  95. package/dist/wallet/keys.d.ts.map +1 -1
  96. package/dist/wallet/keys.js +2 -0
  97. package/dist/wallet/keys.js.map +1 -1
  98. package/package.json +7 -3
  99. package/dist/commands/wallet.test.d.ts +0 -2
  100. package/dist/commands/wallet.test.d.ts.map +0 -1
  101. package/dist/commands/wallet.test.js +0 -68
  102. package/dist/commands/wallet.test.js.map +0 -1
  103. package/dist/nostr/chat.test.d.ts +0 -2
  104. package/dist/nostr/chat.test.d.ts.map +0 -1
  105. package/dist/nostr/chat.test.js +0 -121
  106. package/dist/nostr/chat.test.js.map +0 -1
  107. package/dist/nostr/store.test.d.ts +0 -2
  108. package/dist/nostr/store.test.d.ts.map +0 -1
  109. package/dist/nostr/store.test.js +0 -456
  110. package/dist/nostr/store.test.js.map +0 -1
  111. package/dist/utils/prices.test.d.ts +0 -5
  112. package/dist/utils/prices.test.d.ts.map +0 -1
  113. package/dist/utils/prices.test.js +0 -62
  114. package/dist/utils/prices.test.js.map +0 -1
  115. package/dist/utils/x402.test.d.ts +0 -2
  116. package/dist/utils/x402.test.d.ts.map +0 -1
  117. package/dist/utils/x402.test.js +0 -216
  118. package/dist/utils/x402.test.js.map +0 -1
  119. package/dist/wallet/cauldron/pools.test.d.ts +0 -3
  120. package/dist/wallet/cauldron/pools.test.d.ts.map +0 -1
  121. package/dist/wallet/cauldron/pools.test.js +0 -149
  122. package/dist/wallet/cauldron/pools.test.js.map +0 -1
  123. package/dist/wallet/cauldron/swap.test.d.ts +0 -2
  124. package/dist/wallet/cauldron/swap.test.d.ts.map +0 -1
  125. package/dist/wallet/cauldron/swap.test.js +0 -179
  126. package/dist/wallet/cauldron/swap.test.js.map +0 -1
  127. package/skills/README.md +0 -68
  128. package/skills/paytaca-send/SKILL.md +0 -159
  129. package/skills/paytaca-wallet/SKILL.md +0 -164
  130. package/skills/paytaca-x402/SKILL.md +0 -130
@@ -1,35 +1,24 @@
1
1
  /**
2
- * CLI commands: token list | info | send | send-nft
2
+ * CLI commands: token list | info | price | send | send-nft
3
3
  *
4
4
  * CashTokens support for the Paytaca CLI.
5
- *
6
- * CashTokens are the native token protocol on Bitcoin Cash, supporting
7
- * both fungible tokens (FTs) and non-fungible tokens (NFTs) at the
8
- * consensus layer. Tokens are received at token-aware addresses
9
- * (z-prefix CashAddr format).
10
- *
11
- * Token sends are delegated to watchtower-cash-js BCH.send() with
12
- * the `token` parameter — identical to how paytaca-app handles it.
13
5
  */
14
6
  import chalk from 'chalk';
15
- import { Address } from 'watchtower-cash-js';
16
- import { loadWallet, loadMnemonic } from '../wallet/index.js';
17
- import { fetchAssetPrices, getUsdPerToken, tokenAmountToUsd, formatUsd, } from '../utils/prices.js';
18
- /** Truncate a hex string for display */
19
- function shortHex(hex, len = 8) {
20
- if (hex.length <= len * 2 + 3)
21
- return hex;
22
- return hex.slice(0, len) + '...' + hex.slice(-len);
23
- }
24
- /** Format a token amount with decimals */
25
- function formatTokenAmount(rawAmount, decimals) {
26
- if (decimals === 0)
27
- return rawAmount.toLocaleString('en-US');
28
- const scaled = rawAmount / Math.pow(10, decimals);
29
- return scaled.toLocaleString('en-US', {
30
- minimumFractionDigits: 0,
31
- maximumFractionDigits: decimals,
32
- });
7
+ import { WalletNotConfiguredError, requireWallet } from '../core/context.js';
8
+ import { getTokenBalances, getTokenDetail, isValidBchAddress, isTokenAddress, sendToken, } from '../core/wallet.js';
9
+ import { explorerTxUrl, shortHex } from '../utils/format.js';
10
+ import { formatUsd } from '../utils/prices.js';
11
+ function reportError(err, asJson, contextLabel) {
12
+ if (asJson) {
13
+ console.log(JSON.stringify({ error: err.message || String(err) }, null, 2));
14
+ process.exit(1);
15
+ }
16
+ if (err instanceof WalletNotConfiguredError) {
17
+ console.log(chalk.red(`\n${err.message}\n`));
18
+ process.exit(1);
19
+ }
20
+ console.log(chalk.red(` Error ${contextLabel}: ${err.message || err}`));
21
+ process.exit(1);
33
22
  }
34
23
  export function registerTokenCommands(program) {
35
24
  const token = program
@@ -40,85 +29,61 @@ export function registerTokenCommands(program) {
40
29
  .command('list')
41
30
  .description('List fungible CashTokens in the wallet')
42
31
  .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
32
+ .option('--json', 'Output as JSON')
43
33
  .action(async (opts) => {
44
34
  const isChipnet = Boolean(opts.chipnet);
35
+ const asJson = Boolean(opts.json);
45
36
  const network = isChipnet ? 'chipnet' : 'mainnet';
46
- const data = loadMnemonic();
47
- if (!data) {
48
- console.log(chalk.red('\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'));
49
- process.exit(1);
50
- }
51
- const w = loadWallet();
52
- const bchWallet = w.forNetwork(isChipnet);
53
- console.log(chalk.bold(`\n CashTokens (${network})\n`));
54
37
  try {
55
- const tokens = await bchWallet.getFungibleTokens();
56
- if (tokens.length === 0) {
57
- console.log(chalk.dim(' No tokens found.\n'));
38
+ const result = await getTokenBalances(isChipnet);
39
+ if (asJson) {
40
+ console.log(JSON.stringify(result.tokens, null, 2));
58
41
  return;
59
42
  }
60
- // Fetch USD prices for all held tokens (batched by the util)
61
- let prices = new Map();
62
- try {
63
- const priceData = await fetchAssetPrices(tokens.map((t) => `ct/${t.category}`), ['USD'], isChipnet);
64
- prices = new Map();
65
- for (const p of priceData) {
66
- if (String(p.currency || '').toLowerCase() !== 'usd')
67
- continue;
68
- const catMatch = String(p.asset || '').match(/^ct\/([a-fA-F0-9]+)$/);
69
- if (!catMatch)
70
- continue;
71
- const raw = parseFloat(p.price_value);
72
- if (!isFinite(raw) || raw === 0)
73
- continue;
74
- prices.set(catMatch[1], 1 / raw);
75
- }
76
- }
77
- catch {
78
- // Pricing unavailable — proceed without USD values
43
+ console.log(chalk.bold(`\n CashTokens (${network})\n`));
44
+ if (result.tokens.length === 0) {
45
+ console.log(chalk.dim(' No tokens found.\n'));
46
+ return;
79
47
  }
80
48
  let totalUsd = 0;
81
49
  let pricedCount = 0;
82
- for (const t of tokens) {
83
- const amount = formatTokenAmount(t.balance, t.decimals);
50
+ for (const t of result.tokens) {
84
51
  const symbol = t.symbol ? ` ${t.symbol}` : '';
85
52
  const name = t.name !== 'Unknown Token' ? t.name : '';
86
- const usdPerToken = prices.get(t.category);
87
- console.log(` ${chalk.bold(amount + symbol)}`);
88
- if (name) {
53
+ console.log(` ${chalk.bold(t.displayBalance + symbol)}`);
54
+ if (name)
89
55
  console.log(chalk.dim(` ${name}`));
90
- }
91
56
  console.log(chalk.dim(` ${t.category}`));
92
- if (usdPerToken !== undefined) {
93
- const usdValue = tokenAmountToUsd(t.balance, t.decimals, usdPerToken);
94
- totalUsd += usdValue;
57
+ if (t.usd !== null) {
58
+ totalUsd += t.usd;
95
59
  pricedCount += 1;
96
- console.log(chalk.green(` ≈ ${formatUsd(usdValue)}`));
60
+ console.log(chalk.green(` ≈ ${formatUsd(t.usd)}`));
97
61
  }
98
62
  console.log();
99
63
  }
100
64
  if (pricedCount > 0) {
101
- console.log(chalk.dim(` ${tokens.length} token${tokens.length !== 1 ? 's' : ''} total`));
65
+ console.log(chalk.dim(` ${result.tokens.length} token${result.tokens.length !== 1 ? 's' : ''} total`));
102
66
  console.log(chalk.dim(` ${pricedCount} priced`));
103
67
  console.log(chalk.bold(` Total: ${formatUsd(totalUsd)}`));
104
68
  console.log();
105
69
  }
106
70
  else {
107
- console.log(chalk.dim(` ${tokens.length} token${tokens.length !== 1 ? 's' : ''} total`));
71
+ console.log(chalk.dim(` ${result.tokens.length} token${result.tokens.length !== 1 ? 's' : ''} total`));
108
72
  }
73
+ console.log();
109
74
  }
110
75
  catch (err) {
76
+ if (err instanceof WalletNotConfiguredError)
77
+ reportError(err, asJson, '');
111
78
  const status = err?.response?.status;
112
- if (status === 404) {
79
+ if (status === 404 && !asJson) {
113
80
  console.log(chalk.yellow(' Wallet not yet registered with Watchtower on this network.'));
114
81
  console.log(chalk.dim(' Run `paytaca wallet create` or `paytaca wallet import` to register.'));
82
+ console.log();
83
+ return;
115
84
  }
116
- else {
117
- console.log(chalk.red(` Error fetching tokens: ${err.message || err}`));
118
- process.exit(1);
119
- }
85
+ reportError(err, asJson, 'fetching tokens');
120
86
  }
121
- console.log();
122
87
  });
123
88
  // ── token info ─────────────────────────────────────────────────────
124
89
  token
@@ -126,85 +91,50 @@ export function registerTokenCommands(program) {
126
91
  .description('Show details for a specific CashToken')
127
92
  .argument('<category>', 'Token category ID (64-character hex)')
128
93
  .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
94
+ .option('--json', 'Output as JSON')
129
95
  .action(async (category, opts) => {
130
96
  const isChipnet = Boolean(opts.chipnet);
97
+ const asJson = Boolean(opts.json);
131
98
  const network = isChipnet ? 'chipnet' : 'mainnet';
132
- // Validate category format
133
99
  if (!/^[a-fA-F0-9]{64}$/.test(category)) {
134
100
  console.log(chalk.red('\nError: Category must be a 64-character hex string.\n'));
135
101
  process.exit(1);
136
102
  }
137
- const data = loadMnemonic();
138
- if (!data) {
139
- console.log(chalk.red('\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'));
140
- process.exit(1);
141
- }
142
- const w = loadWallet();
143
- const bchWallet = w.forNetwork(isChipnet);
144
- console.log(chalk.bold(`\n Token Info (${network})\n`));
145
103
  try {
146
- const tokenInfo = await bchWallet.getTokenInfo(category);
147
- if (!tokenInfo) {
148
- console.log(chalk.yellow(' Token not found.\n'));
104
+ const detail = await getTokenDetail(category, isChipnet);
105
+ if (asJson) {
106
+ console.log(JSON.stringify(detail, null, 2));
149
107
  return;
150
108
  }
151
- console.log(` Name: ${tokenInfo.name}`);
152
- if (tokenInfo.symbol) {
153
- console.log(` Symbol: ${tokenInfo.symbol}`);
154
- }
155
- console.log(` Decimals: ${tokenInfo.decimals}`);
156
- console.log(` Category: ${tokenInfo.category}`);
157
- // Fetch token USD price (watchtower.cash asset-prices)
158
- let usdPerToken;
159
- try {
160
- const p = await getUsdPerToken(category, isChipnet);
161
- if (p !== null)
162
- usdPerToken = p;
163
- }
164
- catch {
165
- // Pricing unavailable — omit USD lines
166
- }
167
- // Fetch wallet-specific balance
168
- try {
169
- const balResult = await bchWallet.getTokenBalance(category);
170
- const amount = formatTokenAmount(balResult.balance, tokenInfo.decimals);
171
- const symbol = tokenInfo.symbol ? ` ${tokenInfo.symbol}` : '';
172
- console.log(` Balance: ${amount}${symbol}`);
173
- if (usdPerToken !== undefined) {
174
- const usdValue = tokenAmountToUsd(balResult.balance, tokenInfo.decimals, usdPerToken);
175
- console.log(chalk.green(` Value: ≈ ${formatUsd(usdValue)}`));
176
- }
177
- }
178
- catch {
179
- // Balance may not be available if wallet doesn't hold this token
180
- console.log(chalk.dim(' Balance: 0'));
109
+ console.log(chalk.bold(`\n Token Info (${network})\n`));
110
+ console.log(` Name: ${detail.name}`);
111
+ if (detail.symbol)
112
+ console.log(` Symbol: ${detail.symbol}`);
113
+ console.log(` Decimals: ${detail.decimals}`);
114
+ console.log(` Category: ${detail.category}`);
115
+ const symbol = detail.symbol ? ` ${detail.symbol}` : '';
116
+ console.log(` Balance: ${detail.displayBalance}${symbol}`);
117
+ if (detail.usd !== null) {
118
+ console.log(chalk.green(` Value: ≈ ${formatUsd(detail.usd)}`));
181
119
  }
182
- if (usdPerToken !== undefined) {
183
- console.log(chalk.dim(` Price: ${formatUsd(usdPerToken)} per ${tokenInfo.symbol || 'token'}`));
120
+ if (detail.usdPerToken !== null) {
121
+ console.log(chalk.dim(` Price: ${formatUsd(detail.usdPerToken)} per ${detail.symbol || 'token'}`));
184
122
  }
185
- // Show NFTs for this category
186
- try {
187
- const nfts = await bchWallet.getNftUtxos(category);
188
- if (nfts.length > 0) {
189
- console.log(`\n ${chalk.bold('NFTs')} (${nfts.length})\n`);
190
- for (const nft of nfts) {
191
- const cap = nft.capability === 'none' ? '' : ` [${nft.capability}]`;
192
- const commitment = nft.commitment ? shortHex(nft.commitment) : '(empty)';
193
- console.log(` ${chalk.cyan(commitment)}${cap}`);
194
- console.log(chalk.dim(` ${nft.txid}:${nft.vout}`));
195
- console.log();
196
- }
123
+ if (detail.nfts.length > 0) {
124
+ console.log(`\n ${chalk.bold('NFTs')} (${detail.nfts.length})\n`);
125
+ for (const nft of detail.nfts) {
126
+ const cap = nft.capability === 'none' ? '' : ` [${nft.capability}]`;
127
+ const commitment = nft.commitment ? shortHex(nft.commitment) : '(empty)';
128
+ console.log(` ${chalk.cyan(commitment)}${cap}`);
129
+ console.log(chalk.dim(` ${nft.txid}:${nft.vout}`));
130
+ console.log();
197
131
  }
198
132
  }
199
- catch {
200
- // NFT fetch may fail; non-critical
201
- }
133
+ console.log();
202
134
  }
203
135
  catch (err) {
204
- console.log(chalk.red(` Error: ${err.message || err}`));
205
- process.exit(1);
136
+ reportError(err, asJson, '');
206
137
  }
207
- console.log();
208
138
  });
209
139
  // ── token price ────────────────────────────────────────────────────
210
140
  token
@@ -213,15 +143,15 @@ export function registerTokenCommands(program) {
213
143
  .argument('<category>', 'Token category ID (64-character hex)')
214
144
  .argument('[amount]', 'Token amount in display units (defaults to wallet balance)')
215
145
  .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
146
+ .option('--json', 'Output as JSON')
216
147
  .action(async (category, amountStr, opts) => {
217
148
  const isChipnet = Boolean(opts.chipnet);
149
+ const asJson = Boolean(opts.json);
218
150
  const network = isChipnet ? 'chipnet' : 'mainnet';
219
- // Validate category format
220
151
  if (!/^[a-fA-F0-9]{64}$/.test(category)) {
221
152
  console.log(chalk.red('\nError: Category must be a 64-character hex string.\n'));
222
153
  process.exit(1);
223
154
  }
224
- // Validate amount if provided
225
155
  let requestedAmount = null;
226
156
  if (amountStr !== undefined) {
227
157
  requestedAmount = Number(amountStr);
@@ -230,65 +160,44 @@ export function registerTokenCommands(program) {
230
160
  process.exit(1);
231
161
  }
232
162
  }
233
- const data = loadMnemonic();
234
- if (!data) {
235
- console.log(chalk.red('\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'));
236
- process.exit(1);
237
- }
238
- const w = loadWallet();
239
- const bchWallet = w.forNetwork(isChipnet);
240
- console.log(chalk.bold(`\n Token Price (${network})\n`));
241
- // Resolve token metadata (symbol/decimals) — optional, non-fatal
242
- let symbol = '';
243
- let decimals = 0;
244
- let name = '';
245
163
  try {
246
- const info = await bchWallet.getTokenInfo(category);
247
- if (info) {
248
- symbol = info.symbol || '';
249
- name = info.name !== 'Unknown Token' ? info.name : '';
250
- decimals = info.decimals || 0;
251
- }
252
- }
253
- catch {
254
- // Token metadata unavailable — proceed without it
255
- }
256
- // Resolve the display amount to price
257
- let displayAmount;
258
- if (requestedAmount !== null) {
259
- displayAmount = requestedAmount;
260
- }
261
- else {
262
- try {
263
- const balResult = await bchWallet.getTokenBalance(category);
264
- displayAmount = balResult.balance / Math.pow(10, decimals);
164
+ const detail = await getTokenDetail(category, isChipnet);
165
+ const displayAmount = requestedAmount !== null
166
+ ? requestedAmount
167
+ : detail.rawBalance / Math.pow(10, detail.decimals);
168
+ if (asJson) {
169
+ console.log(JSON.stringify({
170
+ network,
171
+ category,
172
+ symbol: detail.symbol,
173
+ name: detail.name,
174
+ decimals: detail.decimals,
175
+ amount: displayAmount,
176
+ usdPerToken: detail.usdPerToken,
177
+ usd: detail.usdPerToken !== null
178
+ ? displayAmount * detail.usdPerToken
179
+ : null,
180
+ }, null, 2));
181
+ return;
265
182
  }
266
- catch {
267
- console.log(chalk.yellow(' Wallet balance unavailable; specify an <amount> to price.\n'));
268
- process.exit(1);
183
+ const label = detail.symbol || (detail.name !== 'Unknown Token' ? detail.name : '') || shortHex(category);
184
+ console.log(chalk.bold(`\n Token Price (${network})\n`));
185
+ console.log(` Token: ${label}`);
186
+ if (detail.name !== 'Unknown Token')
187
+ console.log(chalk.dim(` Name: ${detail.name}`));
188
+ console.log(chalk.dim(` Category: ${category}`));
189
+ if (detail.usdPerToken === null) {
190
+ console.log(chalk.yellow('\n No market price available for this token.\n'));
191
+ return;
269
192
  }
193
+ const usdValue = displayAmount * detail.usdPerToken;
194
+ console.log(chalk.green(` Price: ${formatUsd(detail.usdPerToken)} per ${detail.symbol || 'token'}`));
195
+ console.log(chalk.bold(` Value: ${formatUsd(usdValue)} for ${displayAmount} ${detail.symbol || 'token(s)'}`));
196
+ console.log();
270
197
  }
271
- // Fetch USD price
272
- let usdPerToken = null;
273
- try {
274
- usdPerToken = await getUsdPerToken(category, isChipnet);
275
- }
276
- catch {
277
- // leave null
278
- }
279
- const label = symbol || name || shortHex(category);
280
- console.log(` Token: ${label}`);
281
- if (name)
282
- console.log(chalk.dim(` Name: ${name}`));
283
- console.log(chalk.dim(` Category: ${category}`));
284
- if (usdPerToken === null) {
285
- console.log(chalk.yellow('\n No market price available for this token.\n'));
286
- return;
198
+ catch (err) {
199
+ reportError(err, asJson, '');
287
200
  }
288
- const usdValue = displayAmount * usdPerToken;
289
- console.log(chalk.green(` Price: ${formatUsd(usdPerToken)} per ${symbol || 'token'}`));
290
- console.log(chalk.bold(` Value: ${formatUsd(usdValue)} for ${displayAmount} ${symbol || 'token(s)'}`));
291
- console.log();
292
201
  });
293
202
  // ── token send ─────────────────────────────────────────────────────
294
203
  token
@@ -298,22 +207,16 @@ export function registerTokenCommands(program) {
298
207
  .argument('<amount>', 'Token amount to send (in base units, before decimal scaling)')
299
208
  .requiredOption('--token <id>', 'Token category ID (64-character hex)')
300
209
  .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
210
+ .option('--json', 'Output as JSON')
301
211
  .action(async (address, amountStr, opts) => {
302
212
  const isChipnet = Boolean(opts.chipnet);
213
+ const asJson = Boolean(opts.json);
303
214
  const category = opts.token;
304
215
  const network = isChipnet ? 'chipnet' : 'mainnet';
305
- // Validate wallet
306
- const data = loadMnemonic();
307
- if (!data) {
308
- console.log(chalk.red('\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'));
309
- process.exit(1);
310
- }
311
- // Validate category
312
216
  if (!/^[a-fA-F0-9]{64}$/.test(category)) {
313
217
  console.log(chalk.red('\nError: Category must be a 64-character hex string.\n'));
314
218
  process.exit(1);
315
219
  }
316
- // Parse amount as bigint
317
220
  let tokenAmount;
318
221
  try {
319
222
  tokenAmount = BigInt(amountStr);
@@ -321,70 +224,69 @@ export function registerTokenCommands(program) {
321
224
  catch {
322
225
  console.log(chalk.red('\nError: Amount must be a valid integer.\n'));
323
226
  process.exit(1);
324
- return; // unreachable, but helps TS narrowing
227
+ return;
325
228
  }
326
229
  if (tokenAmount <= 0n) {
327
230
  console.log(chalk.red('\nError: Amount must be positive.\n'));
328
231
  process.exit(1);
329
232
  }
330
- // Validate recipient address
331
- const addressValidator = new Address(address);
332
- if (!addressValidator.isValidBCHAddress(isChipnet) &&
333
- !addressValidator.isP2SH?.() &&
334
- !addressValidator.isTokenAddress?.()) {
233
+ if (!isValidBchAddress(address, isChipnet)) {
335
234
  console.log(chalk.red('\nError: Invalid BCH address.\n'));
336
235
  process.exit(1);
337
236
  }
338
- // Warn if not a token address
339
- if (!addressValidator.isTokenAddress?.()) {
237
+ if (!isTokenAddress(address) && !asJson) {
340
238
  console.log(chalk.yellow('\n Warning: Address is not a token-aware (z-prefix) address.'));
341
239
  console.log(chalk.yellow(' Tokens should be sent to token-aware addresses to avoid loss.\n'));
342
240
  }
343
- const w = loadWallet();
344
- const bchWallet = w.forNetwork(isChipnet);
345
- // Token change goes to our token address
346
- const tokenChangeAddress = bchWallet.getTokenAddressSetAt(0).change;
347
- // Fetch token info for display
348
- let tokenLabel = shortHex(category);
349
241
  try {
350
- const info = await bchWallet.getTokenInfo(category);
351
- if (info?.symbol)
352
- tokenLabel = info.symbol;
353
- else if (info?.name && info.name !== 'Unknown Token')
354
- tokenLabel = info.name;
355
- }
356
- catch {
357
- // Non-critical; use category hex
358
- }
359
- console.log(`\n Sending ${chalk.bold(amountStr + ' ' + tokenLabel)} on ${chalk.cyan(network)}`);
360
- console.log(chalk.dim(` Category: ${category}`));
361
- console.log(chalk.dim(` To: ${address}`));
362
- console.log(chalk.dim(` Change: ${tokenChangeAddress}`));
363
- console.log();
364
- try {
365
- const result = await bchWallet.sendToken(category, tokenAmount, address, tokenChangeAddress);
366
- if (result.success) {
242
+ let tokenLabel = shortHex(category);
243
+ try {
244
+ const detail = await getTokenDetail(category, isChipnet);
245
+ if (detail.symbol)
246
+ tokenLabel = detail.symbol;
247
+ else if (detail.name !== 'Unknown Token')
248
+ tokenLabel = detail.name;
249
+ }
250
+ catch {
251
+ // non-critical
252
+ }
253
+ if (!asJson) {
254
+ console.log(`\n Sending ${chalk.bold(amountStr + ' ' + tokenLabel)} on ${chalk.cyan(network)}`);
255
+ console.log(chalk.dim(` Category: ${category}`));
256
+ console.log(chalk.dim(` To: ${address}`));
257
+ console.log();
258
+ }
259
+ const outcome = await sendToken({ category, amount: tokenAmount, address }, isChipnet);
260
+ if (asJson) {
261
+ console.log(JSON.stringify({
262
+ ...outcome,
263
+ address,
264
+ category,
265
+ amount: amountStr,
266
+ }, null, 2));
267
+ if (!outcome.success)
268
+ process.exit(1);
269
+ return;
270
+ }
271
+ if (outcome.success) {
367
272
  console.log(chalk.green(' Transaction sent successfully!\n'));
368
- if (result.txid) {
369
- console.log(` txid: ${result.txid}`);
370
- const explorer = isChipnet
371
- ? 'https://chipnet.chaingraph.cash/tx/'
372
- : 'https://bchexplorer.info/tx/';
373
- console.log(chalk.dim(` ${explorer}${result.txid}`));
273
+ if (outcome.txid) {
274
+ console.log(` txid: ${outcome.txid}`);
275
+ console.log(chalk.dim(` ${outcome.explorerUrl}`));
374
276
  }
375
277
  }
376
278
  else {
377
- console.log(chalk.red(` Transaction failed: ${result.error || 'Unknown error'}`));
378
- if (result.lackingSats) {
379
- console.log(chalk.yellow(` Insufficient BCH for transaction fees. Short by ${result.lackingSats} satoshis.`));
279
+ console.log(chalk.red(` Transaction failed: ${outcome.error || 'Unknown error'}`));
280
+ if (outcome.lackingSats) {
281
+ console.log(chalk.yellow(` Insufficient BCH for transaction fees. Short by ${outcome.lackingSats} satoshis.`));
380
282
  }
283
+ process.exit(1);
381
284
  }
285
+ console.log();
382
286
  }
383
287
  catch (err) {
384
- console.log(chalk.red(`\n Error: ${err.message || err}\n`));
385
- process.exit(1);
288
+ reportError(err, asJson, '');
386
289
  }
387
- console.log();
388
290
  });
389
291
  // ── token send-nft ─────────────────────────────────────────────────
390
292
  token
@@ -397,48 +299,40 @@ export function registerTokenCommands(program) {
397
299
  .option('--txid <txid>', 'UTXO txid containing the NFT (auto-detected if omitted)')
398
300
  .option('--vout <n>', 'UTXO output index (auto-detected if omitted)')
399
301
  .option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
302
+ .option('--json', 'Output as JSON')
400
303
  .action(async (address, opts) => {
401
304
  const isChipnet = Boolean(opts.chipnet);
305
+ const asJson = Boolean(opts.json);
402
306
  const category = opts.token;
403
307
  const commitment = opts.commitment;
404
308
  const capability = opts.capability || 'none';
405
309
  const network = isChipnet ? 'chipnet' : 'mainnet';
406
- // Validate wallet
407
- const data = loadMnemonic();
408
- if (!data) {
409
- console.log(chalk.red('\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'));
410
- process.exit(1);
411
- }
412
- // Validate category
413
310
  if (!/^[a-fA-F0-9]{64}$/.test(category)) {
414
311
  console.log(chalk.red('\nError: Category must be a 64-character hex string.\n'));
415
312
  process.exit(1);
416
313
  }
417
- // Validate capability
418
314
  if (!['none', 'minting', 'mutable'].includes(capability)) {
419
315
  console.log(chalk.red('\nError: Capability must be "none", "minting", or "mutable".\n'));
420
316
  process.exit(1);
421
317
  }
422
- // Validate address
423
- const addressValidator = new Address(address);
424
- if (!addressValidator.isValidBCHAddress(isChipnet) &&
425
- !addressValidator.isP2SH?.() &&
426
- !addressValidator.isTokenAddress?.()) {
318
+ if (!isValidBchAddress(address, isChipnet)) {
427
319
  console.log(chalk.red('\nError: Invalid BCH address.\n'));
428
320
  process.exit(1);
429
321
  }
430
- const w = loadWallet();
431
- const bchWallet = w.forNetwork(isChipnet);
432
- // Resolve UTXO either from flags or auto-detect
433
- let txid = opts.txid || '';
434
- let vout = opts.vout !== undefined ? parseInt(opts.vout, 10) : -1;
435
- if (!txid || vout < 0) {
436
- // Auto-detect: find the NFT UTXO matching category + commitment + capability
437
- console.log(chalk.dim('\n Searching for NFT UTXO...'));
438
- try {
439
- const nfts = await bchWallet.getNftUtxos(category);
322
+ try {
323
+ const ctx = requireWallet(isChipnet);
324
+ let txid = opts.txid || '';
325
+ let vout = opts.vout !== undefined ? parseInt(opts.vout, 10) : -1;
326
+ if (!txid || vout < 0) {
327
+ if (!asJson)
328
+ console.log(chalk.dim('\n Searching for NFT UTXO...'));
329
+ const nfts = await ctx.bch.getNftUtxos(category);
440
330
  const match = nfts.find((n) => n.commitment === commitment && n.capability === capability);
441
331
  if (!match) {
332
+ if (asJson) {
333
+ console.log(JSON.stringify({ error: `No NFT found matching category ${category}` }, null, 2));
334
+ process.exit(1);
335
+ }
442
336
  console.log(chalk.red(`\n Error: No NFT found matching category ${shortHex(category)} ` +
443
337
  `with commitment "${commitment}" and capability "${capability}".\n`));
444
338
  console.log(chalk.dim(' Use `paytaca token info <category>` to list available NFTs.\n'));
@@ -447,30 +341,39 @@ export function registerTokenCommands(program) {
447
341
  txid = match.txid;
448
342
  vout = match.vout;
449
343
  }
450
- catch (err) {
451
- console.log(chalk.red(`\n Error searching for NFTs: ${err.message || err}\n`));
452
- process.exit(1);
344
+ const changeAddress = ctx.bch.getTokenAddressSetAt(0).change;
345
+ if (!asJson) {
346
+ console.log(`\n Sending NFT on ${chalk.cyan(network)}`);
347
+ console.log(chalk.dim(` Category: ${category}`));
348
+ console.log(chalk.dim(` Commitment: ${commitment || '(empty)'}`));
349
+ console.log(chalk.dim(` Capability: ${capability}`));
350
+ console.log(chalk.dim(` UTXO: ${shortHex(txid)}:${vout}`));
351
+ console.log(chalk.dim(` To: ${address}`));
352
+ console.log();
353
+ }
354
+ const result = await ctx.bch.sendNft(category, commitment, capability, txid, vout, address, changeAddress);
355
+ const jsonResult = {
356
+ network,
357
+ success: result.success,
358
+ txid: result.txid,
359
+ explorerUrl: result.txid ? explorerTxUrl(result.txid, isChipnet) : undefined,
360
+ error: result.error,
361
+ category,
362
+ commitment,
363
+ capability,
364
+ address,
365
+ };
366
+ if (asJson) {
367
+ console.log(JSON.stringify(jsonResult, null, 2));
368
+ if (!result.success)
369
+ process.exit(1);
370
+ return;
453
371
  }
454
- }
455
- // Change address for leftover BCH
456
- const changeAddress = bchWallet.getTokenAddressSetAt(0).change;
457
- console.log(`\n Sending NFT on ${chalk.cyan(network)}`);
458
- console.log(chalk.dim(` Category: ${category}`));
459
- console.log(chalk.dim(` Commitment: ${commitment || '(empty)'}`));
460
- console.log(chalk.dim(` Capability: ${capability}`));
461
- console.log(chalk.dim(` UTXO: ${shortHex(txid)}:${vout}`));
462
- console.log(chalk.dim(` To: ${address}`));
463
- console.log();
464
- try {
465
- const result = await bchWallet.sendNft(category, commitment, capability, txid, vout, address, changeAddress);
466
372
  if (result.success) {
467
373
  console.log(chalk.green(' Transaction sent successfully!\n'));
468
374
  if (result.txid) {
469
375
  console.log(` txid: ${result.txid}`);
470
- const explorer = isChipnet
471
- ? 'https://chipnet.chaingraph.cash/tx/'
472
- : 'https://bchexplorer.info/tx/';
473
- console.log(chalk.dim(` ${explorer}${result.txid}`));
376
+ console.log(chalk.dim(` ${explorerTxUrl(result.txid, isChipnet)}`));
474
377
  }
475
378
  }
476
379
  else {
@@ -478,13 +381,13 @@ export function registerTokenCommands(program) {
478
381
  if (result.lackingSats) {
479
382
  console.log(chalk.yellow(` Insufficient BCH for transaction fees. Short by ${result.lackingSats} satoshis.`));
480
383
  }
384
+ process.exit(1);
481
385
  }
386
+ console.log();
482
387
  }
483
388
  catch (err) {
484
- console.log(chalk.red(`\n Error: ${err.message || err}\n`));
485
- process.exit(1);
389
+ reportError(err, asJson, '');
486
390
  }
487
- console.log();
488
391
  });
489
392
  }
490
393
  //# sourceMappingURL=token.js.map