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