httpcat-cli 0.0.27 → 0.1.1-rc.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.
Files changed (70) hide show
  1. package/.github/dependabot.yml +2 -0
  2. package/.github/workflows/README.md +37 -4
  3. package/.github/workflows/ci.yml +31 -20
  4. package/.github/workflows/homebrew-tap.yml +1 -1
  5. package/.github/workflows/publish-switch.yml +41 -0
  6. package/.github/workflows/rc-publish.yml +196 -0
  7. package/.github/workflows/release.yml +267 -85
  8. package/README.md +107 -108
  9. package/bun.lock +2933 -0
  10. package/dist/commands/account.d.ts.map +1 -1
  11. package/dist/commands/account.js +14 -7
  12. package/dist/commands/account.js.map +1 -1
  13. package/dist/commands/balances.d.ts.map +1 -0
  14. package/dist/commands/balances.js +171 -0
  15. package/dist/commands/balances.js.map +1 -0
  16. package/dist/commands/buy.d.ts.map +1 -1
  17. package/dist/commands/buy.js +743 -35
  18. package/dist/commands/buy.js.map +1 -1
  19. package/dist/commands/chat.d.ts.map +1 -1
  20. package/dist/commands/chat.js +467 -906
  21. package/dist/commands/chat.js.map +1 -1
  22. package/dist/commands/claim.d.ts.map +1 -0
  23. package/dist/commands/claim.js +65 -0
  24. package/dist/commands/claim.js.map +1 -0
  25. package/dist/commands/create.d.ts.map +1 -1
  26. package/dist/commands/create.js +0 -1
  27. package/dist/commands/create.js.map +1 -1
  28. package/dist/commands/info.d.ts.map +1 -1
  29. package/dist/commands/info.js +143 -38
  30. package/dist/commands/info.js.map +1 -1
  31. package/dist/commands/list.d.ts.map +1 -1
  32. package/dist/commands/list.js +31 -27
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/positions.d.ts.map +1 -1
  35. package/dist/commands/positions.js +178 -106
  36. package/dist/commands/positions.js.map +1 -1
  37. package/dist/commands/sell.d.ts.map +1 -1
  38. package/dist/commands/sell.js +720 -28
  39. package/dist/commands/sell.js.map +1 -1
  40. package/dist/index.js +321 -104
  41. package/dist/index.js.map +1 -1
  42. package/dist/interactive/shell.d.ts.map +1 -1
  43. package/dist/interactive/shell.js +328 -179
  44. package/dist/interactive/shell.js.map +1 -1
  45. package/dist/mcp/tools.d.ts.map +1 -1
  46. package/dist/mcp/tools.js +8 -8
  47. package/dist/mcp/tools.js.map +1 -1
  48. package/dist/utils/constants.d.ts.map +1 -0
  49. package/dist/utils/constants.js +66 -0
  50. package/dist/utils/constants.js.map +1 -0
  51. package/dist/utils/formatting.d.ts.map +1 -1
  52. package/dist/utils/formatting.js +3 -5
  53. package/dist/utils/formatting.js.map +1 -1
  54. package/dist/utils/token-resolver.d.ts.map +1 -1
  55. package/dist/utils/token-resolver.js +70 -68
  56. package/dist/utils/token-resolver.js.map +1 -1
  57. package/dist/utils/validation.d.ts.map +1 -1
  58. package/dist/utils/validation.js +4 -3
  59. package/dist/utils/validation.js.map +1 -1
  60. package/jest.config.js +1 -1
  61. package/package.json +19 -13
  62. package/tests/README.md +0 -1
  63. package/.claude/settings.local.json +0 -41
  64. package/dist/commands/balance.d.ts.map +0 -1
  65. package/dist/commands/balance.js +0 -112
  66. package/dist/commands/balance.js.map +0 -1
  67. package/homebrew-httpcat/Formula/httpcat.rb +0 -18
  68. package/homebrew-httpcat/README.md +0 -31
  69. package/homebrew-httpcat/homebrew-httpcat/Formula/httpcat.rb +0 -18
  70. package/homebrew-httpcat/homebrew-httpcat/README.md +0 -31
@@ -1,52 +1,760 @@
1
- import chalk from 'chalk';
2
- import { validateAmount } from '../utils/validation.js';
3
- import { printBox, formatTokenAmount, formatCurrency } from '../utils/formatting.js';
4
- import { printSuccess, printGraduationProgress } from '../interactive/art.js';
5
- import { resolveTokenId } from '../utils/token-resolver.js';
6
- export const TEST_AMOUNTS = ['0.05', '0.10', '0.20'];
7
- export const PROD_AMOUNTS = ['50', '100', '200'];
8
- export async function buyToken(client, identifier, // Can be token ID (UUID), address (0x...), name, or symbol
9
- amount, isTestMode = true, silent = false // Suppress resolver output for quiet/JSON mode
1
+ import chalk from "chalk";
2
+ import { validateTokenId, validateAmount } from "../utils/validation.js";
3
+ import { printBox, formatTokenAmount, formatCurrency, formatAddress, } from "../utils/formatting.js";
4
+ import { printSuccess, printGraduationProgress } from "../interactive/art.js";
5
+ import { resolveTokenId } from "../utils/token-resolver.js";
6
+ import { getTokenInfo } from "./info.js";
7
+ import { createPublicClient, createWalletClient, http, parseAbi, formatUnits, decodeAbiParameters, decodeFunctionData, decodeEventLog, } from "viem";
8
+ import { privateKeyToAccount } from "viem/accounts";
9
+ import { baseSepolia } from "viem/chains";
10
+ async function simulateAndExplainUniversalRouterRevert(args) {
11
+ try {
12
+ await args.publicClient.call({
13
+ to: args.txTo,
14
+ data: args.txData,
15
+ value: args.txValue,
16
+ account: args.userAddress,
17
+ gas: 3000000n,
18
+ });
19
+ }
20
+ catch (error) {
21
+ const revertData = error?.data ??
22
+ error?.cause?.data ??
23
+ error?.walk?.data ??
24
+ error?.cause?.walk?.data;
25
+ if (!args.silent) {
26
+ if (typeof revertData === 'string' && revertData.startsWith('0x') && revertData.length >= 10) {
27
+ const selector = revertData.slice(0, 10);
28
+ console.log(chalk.yellow(` Universal Router simulation revert selector: ${selector}`));
29
+ console.log(chalk.yellow(` Universal Router simulation revert data: ${revertData}`));
30
+ console.log(chalk.yellow(` (Proceeding to broadcast anyway; some RPCs are flaky for eth_call on Universal Router.)`));
31
+ }
32
+ else {
33
+ console.log(chalk.yellow(` Universal Router simulation reverted (no data).`));
34
+ console.log(chalk.yellow(` (Proceeding to broadcast anyway; some RPCs are flaky for eth_call on Universal Router.)`));
35
+ }
36
+ }
37
+ }
38
+ }
39
+ export const TEST_AMOUNTS = ["0.05", "0.10", "0.20"];
40
+ export const PROD_AMOUNTS = ["50", "100", "200"];
41
+ const ERC20_ABI = parseAbi([
42
+ "function approve(address spender, uint256 amount) returns (bool)",
43
+ "function allowance(address owner, address spender) view returns (uint256)",
44
+ "function balanceOf(address owner) view returns (uint256)",
45
+ "function decimals() view returns (uint8)",
46
+ ]);
47
+ const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
48
+ const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
49
+ const PERMIT2_ABI = parseAbi([
50
+ 'function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)',
51
+ 'function approve(address token, address spender, uint160 amount, uint48 expiration)',
52
+ ]);
53
+ const ERC20_TRANSFER_EVENT_ABI = parseAbi([
54
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
55
+ ]);
56
+ function getErc20NetDeltaFromReceipt(args) {
57
+ const tokenLc = args.token.toLowerCase();
58
+ const userLc = args.user.toLowerCase();
59
+ let delta = 0n;
60
+ const logs = Array.isArray(args.receipt?.logs) ? args.receipt.logs : [];
61
+ for (const log of logs) {
62
+ try {
63
+ const addr = String(log?.address ?? '').toLowerCase();
64
+ if (addr !== tokenLc)
65
+ continue;
66
+ const decoded = decodeEventLog({
67
+ abi: ERC20_TRANSFER_EVENT_ABI,
68
+ data: log.data,
69
+ topics: log.topics,
70
+ });
71
+ if (decoded.eventName !== 'Transfer')
72
+ continue;
73
+ const fromLc = decoded.args.from.toLowerCase();
74
+ const toLc = decoded.args.to.toLowerCase();
75
+ const value = BigInt(decoded.args.value);
76
+ if (fromLc === userLc)
77
+ delta -= value;
78
+ if (toLc === userLc)
79
+ delta += value;
80
+ }
81
+ catch {
82
+ // ignore
83
+ }
84
+ }
85
+ return delta;
86
+ }
87
+ function decodeUniversalRouterExecute(data) {
88
+ const UNIVERSAL_ROUTER_ABI = parseAbi([
89
+ 'function execute(bytes commands, bytes[] inputs, uint256 deadline) external payable',
90
+ ]);
91
+ try {
92
+ const decoded = decodeFunctionData({
93
+ abi: UNIVERSAL_ROUTER_ABI,
94
+ data,
95
+ });
96
+ const [commands, inputs, deadline] = decoded.args;
97
+ return { commands, inputs, deadline };
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ async function ensurePermit2AllowanceIfNeeded(args) {
104
+ const decoded = decodeUniversalRouterExecute(args.txData);
105
+ if (!decoded)
106
+ return;
107
+ let tokenIn = null;
108
+ let amount = null;
109
+ // Find V4_SWAP command and decode first action params (ExactInputSingle)
110
+ const commands = decoded.commands;
111
+ const cmdLen = (commands.length - 2) / 2;
112
+ const cmdBytes = [];
113
+ for (let i = 0; i < cmdLen; i++) {
114
+ cmdBytes.push(commands.slice(2 + i * 2, 4 + i * 2).toLowerCase());
115
+ }
116
+ const v4Index = cmdBytes.findIndex((c) => c === '10');
117
+ const v4Input = v4Index >= 0 ? decoded.inputs[v4Index] : undefined;
118
+ if (v4Input) {
119
+ try {
120
+ const [_, params] = decodeAbiParameters([
121
+ { name: 'actions', type: 'bytes' },
122
+ { name: 'params', type: 'bytes[]' },
123
+ ], v4Input);
124
+ if (params[0]) {
125
+ const [swap] = decodeAbiParameters([
126
+ {
127
+ name: 'swap',
128
+ type: 'tuple',
129
+ components: [
130
+ {
131
+ name: 'poolKey',
132
+ type: 'tuple',
133
+ components: [
134
+ { name: 'currency0', type: 'address' },
135
+ { name: 'currency1', type: 'address' },
136
+ { name: 'fee', type: 'uint24' },
137
+ { name: 'tickSpacing', type: 'int24' },
138
+ { name: 'hooks', type: 'address' },
139
+ ],
140
+ },
141
+ { name: 'zeroForOne', type: 'bool' },
142
+ { name: 'amountIn', type: 'uint128' },
143
+ { name: 'amountOutMinimum', type: 'uint128' },
144
+ { name: 'hookData', type: 'bytes' },
145
+ ],
146
+ },
147
+ ], params[0]);
148
+ const currency0 = (swap.poolKey?.currency0 ?? swap.poolKey?.[0]);
149
+ const currency1 = (swap.poolKey?.currency1 ?? swap.poolKey?.[1]);
150
+ const zeroForOne = (swap.zeroForOne ?? swap[1]);
151
+ tokenIn = (zeroForOne ? currency0 : currency1);
152
+ amount = (swap.amountIn ?? swap[2]);
153
+ }
154
+ }
155
+ catch {
156
+ // ignore
157
+ }
158
+ }
159
+ // Backwards compatibility: if calldata starts with PERMIT2_TRANSFER_FROM, decode token/amount from that.
160
+ const firstCommand = decoded.commands.length >= 4 ? decoded.commands.slice(2, 4).toLowerCase() : '';
161
+ if ((!tokenIn || !amount) && (firstCommand === '02' || firstCommand === '0a') && decoded.inputs.length >= 1) {
162
+ try {
163
+ const decodedPermit2 = decodeAbiParameters([
164
+ { name: 'token', type: 'address' },
165
+ { name: 'recipient', type: 'address' },
166
+ { name: 'amount', type: 'uint160' },
167
+ ], decoded.inputs[0]);
168
+ tokenIn = decodedPermit2[0];
169
+ amount = decodedPermit2[2];
170
+ }
171
+ catch {
172
+ // ignore
173
+ }
174
+ }
175
+ if (!tokenIn || !amount)
176
+ return;
177
+ const toBigInt = (v) => (typeof v === 'bigint' ? v : BigInt(v));
178
+ const normalizeAllowance = (raw) => {
179
+ if (Array.isArray(raw)) {
180
+ const [a, e, n] = raw;
181
+ return {
182
+ amount: toBigInt(a),
183
+ expiration: toBigInt(e),
184
+ nonce: toBigInt(n),
185
+ };
186
+ }
187
+ return {
188
+ amount: toBigInt(raw.amount),
189
+ expiration: toBigInt(raw.expiration),
190
+ nonce: toBigInt(raw.nonce),
191
+ };
192
+ };
193
+ const allowance = normalizeAllowance(await args.publicClient.readContract({
194
+ address: PERMIT2_ADDRESS,
195
+ abi: PERMIT2_ABI,
196
+ functionName: 'allowance',
197
+ args: [args.userAddress, tokenIn, args.universalRouter],
198
+ }));
199
+ const currentAmount = allowance.amount;
200
+ const expiration = allowance.expiration;
201
+ const now = BigInt(Math.floor(Date.now() / 1000));
202
+ const isExpired = expiration === 0n || expiration <= now;
203
+ if (currentAmount >= amount && !isExpired) {
204
+ return;
205
+ }
206
+ if (!args.silent) {
207
+ console.log(chalk.cyan(` Setting Permit2 allowance for Universal Router...`));
208
+ }
209
+ const MAX_UINT160 = (1n << 160n) - 1n;
210
+ const MAX_UINT48 = 281474976710655n;
211
+ const approveTx = await args.walletClient.writeContract({
212
+ address: PERMIT2_ADDRESS,
213
+ abi: PERMIT2_ABI,
214
+ functionName: 'approve',
215
+ args: [tokenIn, args.universalRouter, MAX_UINT160, MAX_UINT48],
216
+ });
217
+ await args.publicClient.waitForTransactionReceipt({ hash: approveTx });
218
+ if (!args.silent) {
219
+ try {
220
+ const updated = normalizeAllowance(await args.publicClient.readContract({
221
+ address: PERMIT2_ADDRESS,
222
+ abi: PERMIT2_ABI,
223
+ functionName: 'allowance',
224
+ args: [args.userAddress, tokenIn, args.universalRouter],
225
+ }));
226
+ console.log(chalk.dim(` Permit2 allowance now: amount=${updated.amount.toString()} expiration=${updated.expiration.toString()}`));
227
+ }
228
+ catch {
229
+ // ignore
230
+ }
231
+ }
232
+ }
233
+ function debugUniversalRouterCalldata(data) {
234
+ const UNIVERSAL_ROUTER_ABI = parseAbi([
235
+ 'function execute(bytes commands, bytes[] inputs, uint256 deadline) external payable',
236
+ ]);
237
+ let decoded;
238
+ try {
239
+ decoded = decodeFunctionData({
240
+ abi: UNIVERSAL_ROUTER_ABI,
241
+ data,
242
+ });
243
+ }
244
+ catch {
245
+ return;
246
+ }
247
+ const [commands, inputs] = decoded.args;
248
+ const cmdLen = (commands.length - 2) / 2;
249
+ console.log(chalk.dim(` UniversalRouter.execute preflight: commands=${commands} (len=${cmdLen}), inputs=${inputs.length}`));
250
+ if (cmdLen === 0) {
251
+ throw new Error('Refusing to send Universal Router tx with empty commands (no-op)');
252
+ }
253
+ const cmdBytes = [];
254
+ for (let i = 0; i < cmdLen; i++) {
255
+ cmdBytes.push(commands.slice(2 + i * 2, 4 + i * 2).toLowerCase());
256
+ }
257
+ const v4Index = cmdBytes.findIndex((c) => c === '10');
258
+ const v4Input = v4Index >= 0 ? inputs[v4Index] : undefined;
259
+ if (!v4Input)
260
+ return;
261
+ try {
262
+ const [actions, params] = decodeAbiParameters([
263
+ { name: 'actions', type: 'bytes' },
264
+ { name: 'params', type: 'bytes[]' },
265
+ ], v4Input);
266
+ const actionsLen = (actions.length - 2) / 2;
267
+ console.log(chalk.dim(` V4 actions payload: actions=${actions} (len=${actionsLen}), params=${params.length}`));
268
+ }
269
+ catch {
270
+ // ignore decode errors
271
+ }
272
+ }
273
+ /**
274
+ * Check price impact and log warning if too high
275
+ * TODO: Add confirmation prompt (needs refactor to work with spinner)
276
+ */
277
+ async function checkPriceImpact(priceImpact, silent) {
278
+ // Just log for now - confirmation prompt conflicts with spinner
279
+ // Will add proper confirmation in future refactor
280
+ return;
281
+ }
282
+ async function executeUniswapBuy(swapData, privateKey, silent) {
283
+ const account = privateKeyToAccount(privateKey);
284
+ const userAddress = account.address;
285
+ // Create clients
286
+ const publicClient = createPublicClient({
287
+ chain: baseSepolia,
288
+ transport: http(),
289
+ });
290
+ const walletClient = createWalletClient({
291
+ account,
292
+ chain: baseSepolia,
293
+ transport: http(),
294
+ });
295
+ // Approve tokens if needed
296
+ for (const approval of swapData.approvals) {
297
+ // Check current allowance first
298
+ const currentAllowance = await publicClient.readContract({
299
+ address: approval.token,
300
+ abi: ERC20_ABI,
301
+ functionName: "allowance",
302
+ args: [userAddress, approval.spender],
303
+ });
304
+ const requiredAmount = BigInt(approval.amount);
305
+ // Only approve if current allowance is insufficient
306
+ if (currentAllowance < requiredAmount) {
307
+ if (!silent) {
308
+ console.log(chalk.cyan(` Approving ${approval.token}...`));
309
+ }
310
+ // Approve max uint256 for better UX (avoid repeated approvals)
311
+ const maxUint256 = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
312
+ const approveTx = await walletClient.writeContract({
313
+ address: approval.token,
314
+ abi: ERC20_ABI,
315
+ functionName: "approve",
316
+ args: [approval.spender, maxUint256],
317
+ });
318
+ await publicClient.waitForTransactionReceipt({ hash: approveTx });
319
+ }
320
+ else if (!silent) {
321
+ console.log(chalk.dim(` ✓ Already approved ${approval.token}`));
322
+ }
323
+ }
324
+ // Execute swap using raw transaction data
325
+ if (!silent) {
326
+ console.log(chalk.cyan(` Executing swap via ${swapData.route}...`));
327
+ }
328
+ const usdcBefore = (await publicClient.readContract({
329
+ address: USDC_ADDRESS,
330
+ abi: ERC20_ABI,
331
+ functionName: 'balanceOf',
332
+ args: [userAddress],
333
+ }));
334
+ const tokenBefore = (await publicClient.readContract({
335
+ address: swapData.tokenAddress,
336
+ abi: ERC20_ABI,
337
+ functionName: 'balanceOf',
338
+ args: [userAddress],
339
+ }));
340
+ let txData;
341
+ // Check if we have args (old V2 format) or encoded data
342
+ if (swapData.swap.args && swapData.swap.functionName) {
343
+ // Old format - encode the transaction ourselves
344
+ const { encodeFunctionData } = await import('viem');
345
+ const V2_ROUTER_ABI = [
346
+ {
347
+ inputs: [
348
+ { name: "amountIn", type: "uint256" },
349
+ { name: "amountOutMin", type: "uint256" },
350
+ { name: "path", type: "address[]" },
351
+ { name: "to", type: "address" },
352
+ { name: "deadline", type: "uint256" },
353
+ ],
354
+ name: "swapExactTokensForTokens",
355
+ outputs: [{ name: "amounts", type: "uint256[]" }],
356
+ stateMutability: "nonpayable",
357
+ type: "function",
358
+ },
359
+ ];
360
+ // Replace {{USER_ADDRESS}} in args
361
+ const args = swapData.swap.args.map((arg) => arg === "{{USER_ADDRESS}}" ? userAddress : arg);
362
+ txData = encodeFunctionData({
363
+ abi: V2_ROUTER_ABI,
364
+ functionName: "swapExactTokensForTokens",
365
+ args: [
366
+ BigInt(args[0]),
367
+ BigInt(args[1]),
368
+ args[2],
369
+ args[3],
370
+ BigInt(args[4]),
371
+ ],
372
+ });
373
+ }
374
+ else {
375
+ // New format - use encoded data
376
+ txData = swapData.swap.data;
377
+ }
378
+ if (!silent) {
379
+ debugUniversalRouterCalldata(txData);
380
+ }
381
+ await ensurePermit2AllowanceIfNeeded({
382
+ txData,
383
+ universalRouter: swapData.swap.to,
384
+ userAddress,
385
+ publicClient,
386
+ walletClient,
387
+ silent,
388
+ });
389
+ await simulateAndExplainUniversalRouterRevert({
390
+ txTo: swapData.swap.to,
391
+ txData,
392
+ txValue: BigInt(swapData.swap.value),
393
+ userAddress,
394
+ publicClient,
395
+ silent,
396
+ });
397
+ // Retry logic for swap execution
398
+ let swapTx;
399
+ let lastError = null;
400
+ const maxRetries = 3;
401
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
402
+ try {
403
+ const send = async (gas) => walletClient.sendTransaction({
404
+ to: swapData.swap.to,
405
+ data: txData,
406
+ value: BigInt(swapData.swap.value),
407
+ ...(gas ? { gas } : {}),
408
+ });
409
+ try {
410
+ swapTx = await send();
411
+ }
412
+ catch (inner) {
413
+ const msg = String(inner?.message ?? '');
414
+ if (msg.toLowerCase().includes('execution reverted')) {
415
+ if (!silent) {
416
+ console.log(chalk.yellow(` ⚠️ Estimation reverted; force-broadcasting with manual gas limit for debugging...`));
417
+ }
418
+ swapTx = await send(1500000n);
419
+ }
420
+ else {
421
+ throw inner;
422
+ }
423
+ }
424
+ break; // Success, exit retry loop
425
+ }
426
+ catch (error) {
427
+ lastError = error;
428
+ if (attempt < maxRetries && error.message?.includes('transferFrom failed')) {
429
+ if (!silent) {
430
+ console.log(chalk.yellow(` ⚠️ Swap failed (attempt ${attempt}/${maxRetries}), retrying...`));
431
+ }
432
+ // Wait a bit before retrying
433
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
434
+ }
435
+ else {
436
+ throw error; // Max retries reached or different error
437
+ }
438
+ }
439
+ }
440
+ if (!swapTx) {
441
+ throw lastError || new Error('Swap failed after retries');
442
+ }
443
+ const receipt = await publicClient.waitForTransactionReceipt({
444
+ hash: swapTx,
445
+ });
446
+ if (!silent) {
447
+ console.log(chalk.green(` ✅ Swap complete!`));
448
+ }
449
+ if (!silent) {
450
+ try {
451
+ const usdcDeltaFromLogs = getErc20NetDeltaFromReceipt({
452
+ receipt,
453
+ token: USDC_ADDRESS,
454
+ user: userAddress,
455
+ });
456
+ const tokenDeltaFromLogs = getErc20NetDeltaFromReceipt({
457
+ receipt,
458
+ token: swapData.tokenAddress,
459
+ user: userAddress,
460
+ });
461
+ // For buys:
462
+ // - USDC delta is typically negative (spent)
463
+ // - token delta is typically positive (received)
464
+ let usdcDelta = usdcDeltaFromLogs;
465
+ let tokenDelta = tokenDeltaFromLogs;
466
+ // Fallback to balance snapshots if logs don't include transfers
467
+ if (usdcDelta === 0n || tokenDelta === 0n) {
468
+ try {
469
+ const usdcAfter = (await publicClient.readContract({
470
+ address: USDC_ADDRESS,
471
+ abi: ERC20_ABI,
472
+ functionName: 'balanceOf',
473
+ args: [userAddress],
474
+ }));
475
+ const tokenAfter = (await publicClient.readContract({
476
+ address: swapData.tokenAddress,
477
+ abi: ERC20_ABI,
478
+ functionName: 'balanceOf',
479
+ args: [userAddress],
480
+ }));
481
+ if (usdcDelta === 0n)
482
+ usdcDelta = usdcAfter - usdcBefore;
483
+ if (tokenDelta === 0n)
484
+ tokenDelta = tokenAfter - tokenBefore;
485
+ }
486
+ catch {
487
+ // ignore
488
+ }
489
+ }
490
+ let tokenDecimals = 18;
491
+ try {
492
+ tokenDecimals = Number(await publicClient.readContract({
493
+ address: swapData.tokenAddress,
494
+ abi: ERC20_ABI,
495
+ functionName: 'decimals',
496
+ }));
497
+ }
498
+ catch {
499
+ // ignore
500
+ }
501
+ console.log(chalk.dim(` Actual USDC delta: ${formatUnits(usdcDelta, 6)} (raw=${usdcDelta.toString()})`));
502
+ console.log(chalk.dim(` Actual token delta: ${formatUnits(tokenDelta, tokenDecimals)} (raw=${tokenDelta.toString()})`));
503
+ }
504
+ catch {
505
+ // ignore
506
+ }
507
+ }
508
+ return swapTx;
509
+ }
510
+ export async function buyToken(client, identifier, // Can be token ID, name, or symbol
511
+ amount, isTestMode = true, silent = false, // Suppress resolver output for quiet/JSON mode
512
+ privateKey // Private key for executing Uniswap swaps
10
513
  ) {
11
- // Resolve/validate identifier (returns identifier as-is, backend will resolve)
12
- const tokenIdentifier = await resolveTokenId(identifier, client, silent);
13
- // Validate amount
514
+ // Resolve identifier to token ID (or contract address for external tokens)
515
+ const tokenId = await resolveTokenId(identifier, client, silent);
516
+ // Validate input
517
+ validateTokenId(tokenId);
518
+ // Check if it's a contract address
519
+ const addressRegex = /^0x[0-9a-f]{40}$/i;
520
+ const isContractAddress = addressRegex.test(tokenId);
521
+ // If it's a contract address, try to look it up in the database first
522
+ // It might be one of our tokens (bonding curve or graduated)
523
+ if (isContractAddress) {
524
+ try {
525
+ // Try to get token info - if it exists in our DB, use normal flow
526
+ const tokenInfo = await getTokenInfo(client, tokenId, undefined, silent);
527
+ // It's one of our tokens! Continue with normal flow based on status
528
+ if (tokenInfo.status === "graduating") {
529
+ throw new Error("Token is currently graduating. Please wait for graduation to complete.");
530
+ }
531
+ if (tokenInfo.status === "graduated") {
532
+ // Graduated token - use universal swap
533
+ const amountNum = parseFloat(amount);
534
+ if (isNaN(amountNum) || amountNum <= 0) {
535
+ throw new Error("Amount must be a positive number");
536
+ }
537
+ if (!privateKey) {
538
+ throw new Error("Private key required for Uniswap V2 swaps");
539
+ }
540
+ // Get user address from private key
541
+ const account = privateKeyToAccount(privateKey);
542
+ const userAddress = account.address;
543
+ // Get transaction data from backend using universal swap
544
+ const input = { tokenIdentifier: tokenId, amount, slippage: 0.5, recipient: userAddress };
545
+ const { data } = await client.invoke("token_buy_universal", input);
546
+ // Check price impact (confirmation disabled for now)
547
+ if (data.priceImpact) {
548
+ await checkPriceImpact(data.priceImpact, silent);
549
+ }
550
+ // Execute the swap on-chain
551
+ const txHash = await executeUniswapBuy(data, privateKey, silent);
552
+ return {
553
+ tokenId: data.tokenId,
554
+ tokenAddress: data.tokenAddress,
555
+ tokenName: data.tokenName,
556
+ tokenSymbol: data.tokenSymbol,
557
+ tokensReceived: data.expectedTokens,
558
+ amountSpent: data.usdcAmount,
559
+ newPrice: data.pricePerToken,
560
+ source: "uniswap_v2",
561
+ txHash,
562
+ slippage: data.slippage,
563
+ priceImpact: data.priceImpact,
564
+ route: data.route,
565
+ routeFee: data.fee,
566
+ };
567
+ }
568
+ // Active token - fall through to bonding curve logic below
569
+ }
570
+ catch (error) {
571
+ // Token not in our database - treat as external token
572
+ if (error.message?.includes('not found') || error.message?.includes('Invalid UUID')) {
573
+ // External token - use universal swap
574
+ const amountNum = parseFloat(amount);
575
+ if (isNaN(amountNum) || amountNum <= 0) {
576
+ throw new Error("Amount must be a positive number");
577
+ }
578
+ if (!privateKey) {
579
+ throw new Error("Private key required for swaps");
580
+ }
581
+ // Get user address from private key
582
+ const account = privateKeyToAccount(privateKey);
583
+ const userAddress = account.address;
584
+ // Get transaction data from backend using universal swap
585
+ const input = { tokenIdentifier: tokenId, amount, slippage: 0.5, recipient: userAddress };
586
+ const { data } = await client.invoke("token_buy_universal", input);
587
+ // Check price impact (confirmation disabled for now)
588
+ if (data.priceImpact) {
589
+ await checkPriceImpact(data.priceImpact, silent);
590
+ }
591
+ // Execute the swap on-chain
592
+ const txHash = await executeUniswapBuy(data, privateKey, silent);
593
+ return {
594
+ tokenId: data.tokenId,
595
+ tokenAddress: data.tokenAddress,
596
+ tokenName: data.tokenName,
597
+ tokenSymbol: data.tokenSymbol,
598
+ tokensReceived: data.expectedTokens,
599
+ amountSpent: data.usdcAmount,
600
+ newPrice: data.pricePerToken,
601
+ source: "uniswap_v2",
602
+ txHash,
603
+ slippage: data.slippage,
604
+ priceImpact: data.priceImpact,
605
+ route: data.route,
606
+ routeFee: data.fee,
607
+ };
608
+ }
609
+ else {
610
+ // Some other error - rethrow
611
+ throw error;
612
+ }
613
+ }
614
+ }
615
+ // For UUID tokens or contract addresses that are in our DB, get token info
616
+ const tokenInfo = isContractAddress
617
+ ? await getTokenInfo(client, tokenId, undefined, silent)
618
+ : await getTokenInfo(client, tokenId, undefined, silent);
619
+ // Check token status and route accordingly
620
+ if (tokenInfo.status === "graduating") {
621
+ throw new Error("Token is currently graduating. Please wait for graduation to complete.");
622
+ }
623
+ if (tokenInfo.status === "graduated") {
624
+ // Use Uniswap V2 for graduated tokens - any amount allowed
625
+ const amountNum = parseFloat(amount);
626
+ if (isNaN(amountNum) || amountNum <= 0) {
627
+ throw new Error("Amount must be a positive number");
628
+ }
629
+ if (!privateKey) {
630
+ throw new Error("Private key required for Uniswap V2 swaps");
631
+ }
632
+ // Get transaction data from backend using universal swap
633
+ const input = { tokenIdentifier: tokenId, amount, slippage: 0.5 };
634
+ const { data } = await client.invoke("token_buy_universal", input);
635
+ // Check price impact (confirmation disabled for now)
636
+ if (data.priceImpact) {
637
+ await checkPriceImpact(data.priceImpact, silent);
638
+ }
639
+ // Execute the swap on-chain
640
+ const txHash = await executeUniswapBuy(data, privateKey, silent);
641
+ return {
642
+ tokenId: data.tokenId,
643
+ tokenAddress: data.tokenAddress,
644
+ tokenName: data.tokenName,
645
+ tokenSymbol: data.tokenSymbol,
646
+ tokensReceived: data.expectedTokens,
647
+ amountSpent: data.usdcAmount,
648
+ newPrice: data.pricePerToken,
649
+ source: "uniswap_v2",
650
+ txHash,
651
+ slippage: data.slippage,
652
+ priceImpact: data.priceImpact,
653
+ route: data.route,
654
+ routeFee: data.fee,
655
+ };
656
+ }
657
+ // Active token - use bonding curve (existing logic)
14
658
  const validAmounts = isTestMode ? TEST_AMOUNTS : PROD_AMOUNTS;
15
659
  const normalizedAmount = validateAmount(amount, validAmounts, isTestMode);
16
- const input = { tokenIdentifier, amount: normalizedAmount };
660
+ const input = {
661
+ tokenIdentifier: tokenId,
662
+ amount: normalizedAmount,
663
+ };
17
664
  // Route to correct entrypoint based on test mode and amount
18
665
  // Test mode: token_buy_0_05, token_buy_0_10, token_buy_0_20
19
666
  // Production mode: token_buy (single entrypoint)
20
- const entrypoint = isTestMode ? `token_buy_${normalizedAmount.replace('.', '_')}` : 'token_buy';
667
+ const entrypoint = isTestMode
668
+ ? `token_buy_${normalizedAmount.replace(".", "_")}`
669
+ : "token_buy";
21
670
  // Make request
22
671
  const { data } = await client.invoke(entrypoint, input);
23
- return data;
672
+ // Add token info and source to result
673
+ return {
674
+ ...data,
675
+ tokenAddress: tokenInfo.address,
676
+ tokenName: tokenInfo.name,
677
+ tokenSymbol: tokenInfo.symbol,
678
+ source: "bonding_curve",
679
+ };
24
680
  }
25
681
  export function displayBuyResult(result) {
26
- printSuccess('Tokens purchased successfully!', 'money');
27
- const graduationStatus = result.graduationReached
28
- ? '✅ GRADUATED!'
29
- : `${result.graduationProgress.toFixed(2)}%`;
30
- printBox('💰 Purchase Summary', {
31
- 'Tokens Received': formatTokenAmount(result.tokensReceived),
32
- 'Amount Spent': formatCurrency(result.amountSpent),
33
- 'Fee (1%)': formatCurrency(result.fee),
34
- 'New Price': formatCurrency(result.newPrice),
35
- 'New Market Cap': formatCurrency(result.newMcap),
36
- 'Graduation': graduationStatus,
37
- });
38
- console.log();
39
- printGraduationProgress(result.graduationProgress);
682
+ printSuccess("Tokens purchased successfully!", "money");
683
+ if (result.source === "uniswap_v2") {
684
+ // Graduated token - Uniswap V2 display
685
+ const boxContent = {
686
+ Token: `${result.tokenName} (${result.tokenSymbol})`,
687
+ Contract: formatAddress(result.tokenAddress || ""),
688
+ Source: result.route ?? "Uniswap",
689
+ "Tokens Received": formatTokenAmount(result.tokensReceived),
690
+ "Amount Spent": formatCurrency(result.amountSpent, 6),
691
+ Price: formatCurrency(result.newPrice),
692
+ };
693
+ if (result.slippage !== undefined) {
694
+ boxContent["Slippage"] = `${result.slippage}%`;
695
+ }
696
+ if (result.txHash) {
697
+ boxContent["Transaction"] = result.txHash;
698
+ }
699
+ printBox("💰 Purchase Summary", boxContent);
700
+ // Display price impact warning
701
+ if (result.priceImpact !== undefined) {
702
+ console.log();
703
+ if (result.priceImpact < 0.5) {
704
+ console.log(chalk.green(` Price Impact: ${result.priceImpact.toFixed(2)}%`));
705
+ }
706
+ else if (result.priceImpact < 1) {
707
+ console.log(chalk.yellow(` Price Impact: ${result.priceImpact.toFixed(2)}%`));
708
+ }
709
+ else if (result.priceImpact < 3) {
710
+ console.log(chalk.red(` ⚠️ HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
711
+ console.log(chalk.yellow(` Your trade will move the market significantly.`));
712
+ }
713
+ else {
714
+ console.log(chalk.red(` 🚨 VERY HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
715
+ console.log(chalk.yellow(` Consider splitting into smaller trades.`));
716
+ }
717
+ }
718
+ // Display route info
719
+ if (result.route) {
720
+ const feePercent = result.routeFee ? (result.routeFee / 10000).toFixed(2) : 'N/A';
721
+ console.log(chalk.cyan(` Route: ${result.route} (${feePercent}% fee)`));
722
+ }
723
+ }
724
+ else {
725
+ // Active token - Bonding curve display
726
+ const graduationStatus = result.graduationReached
727
+ ? "✅ GRADUATED!"
728
+ : `${(result.graduationProgress || 0).toFixed(2)}%`;
729
+ const boxContent = {
730
+ Token: `${result.tokenName} (${result.tokenSymbol})`,
731
+ Contract: formatAddress(result.tokenAddress || ""),
732
+ Source: "Bonding Curve",
733
+ "Tokens Received": formatTokenAmount(result.tokensReceived),
734
+ "Amount Spent": formatCurrency(result.amountSpent, 6),
735
+ };
736
+ if (result.fee) {
737
+ boxContent["Fee (1%)"] = formatCurrency(result.fee, 6);
738
+ }
739
+ boxContent["New Price"] = formatCurrency(result.newPrice);
740
+ if (result.newMcap) {
741
+ boxContent["New Market Cap"] = formatCurrency(result.newMcap);
742
+ }
743
+ boxContent["Graduation"] = graduationStatus;
744
+ printBox("💰 Purchase Summary", boxContent);
745
+ console.log();
746
+ if (result.graduationProgress !== undefined) {
747
+ printGraduationProgress(result.graduationProgress);
748
+ }
749
+ }
40
750
  }
41
751
  export function displayBuyResultCompact(result, buyNumber, totalBuys) {
42
752
  const graduationStatus = result.graduationReached
43
- ? '✅ GRADUATED!'
44
- : `${result.graduationProgress.toFixed(2)}%`;
45
- const prefix = buyNumber && totalBuys
46
- ? chalk.dim(`[${buyNumber}/${totalBuys}] `)
47
- : '';
753
+ ? "✅ GRADUATED!"
754
+ : `${(result.graduationProgress || 0).toFixed(2)}%`;
755
+ const prefix = buyNumber && totalBuys ? chalk.dim(`[${buyNumber}/${totalBuys}] `) : "";
48
756
  console.log(prefix +
49
- chalk.green('✅ Buy successful! ') +
757
+ chalk.green("✅ Buy successful! ") +
50
758
  `Received ${chalk.cyan(formatTokenAmount(result.tokensReceived))} tokens, ` +
51
759
  `spent ${chalk.yellow(formatCurrency(result.amountSpent))}, ` +
52
760
  `new price ${chalk.cyan(formatCurrency(result.newPrice))}, ` +