fourmm 0.1.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 (151) hide show
  1. package/README.md +147 -0
  2. package/dist/bin.d.ts +9 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +14 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/cli.d.ts +319 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +25 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/config.d.ts +35 -0
  11. package/dist/commands/config.d.ts.map +1 -0
  12. package/dist/commands/config.js +145 -0
  13. package/dist/commands/config.js.map +1 -0
  14. package/dist/commands/query.d.ts +51 -0
  15. package/dist/commands/query.d.ts.map +1 -0
  16. package/dist/commands/query.js +364 -0
  17. package/dist/commands/query.js.map +1 -0
  18. package/dist/commands/token.d.ts +55 -0
  19. package/dist/commands/token.d.ts.map +1 -0
  20. package/dist/commands/token.js +650 -0
  21. package/dist/commands/token.js.map +1 -0
  22. package/dist/commands/tools.d.ts +54 -0
  23. package/dist/commands/tools.d.ts.map +1 -0
  24. package/dist/commands/tools.js +499 -0
  25. package/dist/commands/tools.js.map +1 -0
  26. package/dist/commands/trade.d.ts +63 -0
  27. package/dist/commands/trade.d.ts.map +1 -0
  28. package/dist/commands/trade.js +933 -0
  29. package/dist/commands/trade.js.map +1 -0
  30. package/dist/commands/transfer.d.ts +51 -0
  31. package/dist/commands/transfer.d.ts.map +1 -0
  32. package/dist/commands/transfer.js +728 -0
  33. package/dist/commands/transfer.js.map +1 -0
  34. package/dist/commands/wallet.d.ts +111 -0
  35. package/dist/commands/wallet.d.ts.map +1 -0
  36. package/dist/commands/wallet.js +716 -0
  37. package/dist/commands/wallet.js.map +1 -0
  38. package/dist/contracts/erc20.d.ts +72 -0
  39. package/dist/contracts/erc20.d.ts.map +1 -0
  40. package/dist/contracts/erc20.js +55 -0
  41. package/dist/contracts/erc20.js.map +1 -0
  42. package/dist/contracts/fourmemeMmRouter.d.ts +68 -0
  43. package/dist/contracts/fourmemeMmRouter.d.ts.map +1 -0
  44. package/dist/contracts/fourmemeMmRouter.js +48 -0
  45. package/dist/contracts/fourmemeMmRouter.js.map +1 -0
  46. package/dist/contracts/pancakeRouter.d.ts +73 -0
  47. package/dist/contracts/pancakeRouter.d.ts.map +1 -0
  48. package/dist/contracts/pancakeRouter.js +50 -0
  49. package/dist/contracts/pancakeRouter.js.map +1 -0
  50. package/dist/contracts/tokenManager2.d.ts +193 -0
  51. package/dist/contracts/tokenManager2.d.ts.map +1 -0
  52. package/dist/contracts/tokenManager2.js +108 -0
  53. package/dist/contracts/tokenManager2.js.map +1 -0
  54. package/dist/contracts/tokenManagerHelper3.d.ts +118 -0
  55. package/dist/contracts/tokenManagerHelper3.d.ts.map +1 -0
  56. package/dist/contracts/tokenManagerHelper3.js +66 -0
  57. package/dist/contracts/tokenManagerHelper3.js.map +1 -0
  58. package/dist/datastore/cache.d.ts +20 -0
  59. package/dist/datastore/cache.d.ts.map +1 -0
  60. package/dist/datastore/cache.js +45 -0
  61. package/dist/datastore/cache.js.map +1 -0
  62. package/dist/datastore/index.d.ts +85 -0
  63. package/dist/datastore/index.d.ts.map +1 -0
  64. package/dist/datastore/index.js +341 -0
  65. package/dist/datastore/index.js.map +1 -0
  66. package/dist/datastore/paths.d.ts +17 -0
  67. package/dist/datastore/paths.d.ts.map +1 -0
  68. package/dist/datastore/paths.js +39 -0
  69. package/dist/datastore/paths.js.map +1 -0
  70. package/dist/datastore/types.d.ts +105 -0
  71. package/dist/datastore/types.d.ts.map +1 -0
  72. package/dist/datastore/types.js +8 -0
  73. package/dist/datastore/types.js.map +1 -0
  74. package/dist/fourmeme/auth.d.ts +22 -0
  75. package/dist/fourmeme/auth.d.ts.map +1 -0
  76. package/dist/fourmeme/auth.js +78 -0
  77. package/dist/fourmeme/auth.js.map +1 -0
  78. package/dist/fourmeme/create.d.ts +31 -0
  79. package/dist/fourmeme/create.d.ts.map +1 -0
  80. package/dist/fourmeme/create.js +111 -0
  81. package/dist/fourmeme/create.js.map +1 -0
  82. package/dist/fourmeme/upload.d.ts +16 -0
  83. package/dist/fourmeme/upload.d.ts.map +1 -0
  84. package/dist/fourmeme/upload.js +52 -0
  85. package/dist/fourmeme/upload.js.map +1 -0
  86. package/dist/lib/bundle.d.ts +51 -0
  87. package/dist/lib/bundle.d.ts.map +1 -0
  88. package/dist/lib/bundle.js +95 -0
  89. package/dist/lib/bundle.js.map +1 -0
  90. package/dist/lib/config.d.ts +58 -0
  91. package/dist/lib/config.d.ts.map +1 -0
  92. package/dist/lib/config.js +183 -0
  93. package/dist/lib/config.js.map +1 -0
  94. package/dist/lib/const.d.ts +165 -0
  95. package/dist/lib/const.d.ts.map +1 -0
  96. package/dist/lib/const.js +98 -0
  97. package/dist/lib/const.js.map +1 -0
  98. package/dist/lib/env.d.ts +14 -0
  99. package/dist/lib/env.d.ts.map +1 -0
  100. package/dist/lib/env.js +18 -0
  101. package/dist/lib/env.js.map +1 -0
  102. package/dist/lib/guards.d.ts +44 -0
  103. package/dist/lib/guards.d.ts.map +1 -0
  104. package/dist/lib/guards.js +65 -0
  105. package/dist/lib/guards.js.map +1 -0
  106. package/dist/lib/identify.d.ts +85 -0
  107. package/dist/lib/identify.d.ts.map +1 -0
  108. package/dist/lib/identify.js +88 -0
  109. package/dist/lib/identify.js.map +1 -0
  110. package/dist/lib/pricing.d.ts +62 -0
  111. package/dist/lib/pricing.d.ts.map +1 -0
  112. package/dist/lib/pricing.js +302 -0
  113. package/dist/lib/pricing.js.map +1 -0
  114. package/dist/lib/routing.d.ts +57 -0
  115. package/dist/lib/routing.d.ts.map +1 -0
  116. package/dist/lib/routing.js +67 -0
  117. package/dist/lib/routing.js.map +1 -0
  118. package/dist/lib/slippage.d.ts +29 -0
  119. package/dist/lib/slippage.d.ts.map +1 -0
  120. package/dist/lib/slippage.js +110 -0
  121. package/dist/lib/slippage.js.map +1 -0
  122. package/dist/lib/tracker.d.ts +68 -0
  123. package/dist/lib/tracker.d.ts.map +1 -0
  124. package/dist/lib/tracker.js +155 -0
  125. package/dist/lib/tracker.js.map +1 -0
  126. package/dist/lib/viem.d.ts +12 -0
  127. package/dist/lib/viem.d.ts.map +1 -0
  128. package/dist/lib/viem.js +44 -0
  129. package/dist/lib/viem.js.map +1 -0
  130. package/dist/lib/wallet-rows.d.ts +30 -0
  131. package/dist/lib/wallet-rows.d.ts.map +1 -0
  132. package/dist/lib/wallet-rows.js +9 -0
  133. package/dist/lib/wallet-rows.js.map +1 -0
  134. package/dist/lib/walletClient.d.ts +16 -0
  135. package/dist/lib/walletClient.d.ts.map +1 -0
  136. package/dist/lib/walletClient.js +26 -0
  137. package/dist/lib/walletClient.js.map +1 -0
  138. package/dist/wallets/groups/encrypt.d.ts +26 -0
  139. package/dist/wallets/groups/encrypt.d.ts.map +1 -0
  140. package/dist/wallets/groups/encrypt.js +52 -0
  141. package/dist/wallets/groups/encrypt.js.map +1 -0
  142. package/dist/wallets/groups/generate.d.ts +19 -0
  143. package/dist/wallets/groups/generate.d.ts.map +1 -0
  144. package/dist/wallets/groups/generate.js +36 -0
  145. package/dist/wallets/groups/generate.js.map +1 -0
  146. package/dist/wallets/groups/store.d.ts +107 -0
  147. package/dist/wallets/groups/store.d.ts.map +1 -0
  148. package/dist/wallets/groups/store.js +254 -0
  149. package/dist/wallets/groups/store.js.map +1 -0
  150. package/package.json +50 -0
  151. package/skills/SKILL.md +187 -0
@@ -0,0 +1,933 @@
1
+ /**
2
+ * `fourmm trade` command group — buy, sell, sniper, batch.
3
+ *
4
+ * Week 3: Router deployed at 0x5A3A...84C1. Live broadcast enabled for
5
+ * buy / sell on bonding-curve tokens. PancakeSwap graduated path uses
6
+ * Router.volumePancake() for batch commands.
7
+ *
8
+ * All trade commands enforce:
9
+ * - assertSupportedToken (refuses TaxToken / X Mode)
10
+ * - resolveTradingPath (bonding-curve vs pancake)
11
+ * - tradeable check (launchTime <= now)
12
+ */
13
+ import { Cli, z } from 'incur';
14
+ import { createWalletClient, fallback, formatEther, formatUnits, getAddress, http, isAddress, parseEther, parseUnits, } from 'viem';
15
+ import { privateKeyToAccount } from 'viem/accounts';
16
+ import { bsc, bscTestnet } from 'viem/chains';
17
+ import { PANCAKE_V2_ROUTER, TOKEN_MANAGER_HELPER3, WBNB, requireRouter } from '../lib/const.js';
18
+ import { loadConfig } from '../lib/config.js';
19
+ import { tokenManagerHelper3Abi } from '../contracts/tokenManagerHelper3.js';
20
+ import { tokenManager2Abi } from '../contracts/tokenManager2.js';
21
+ import { erc20Abi } from '../contracts/erc20.js';
22
+ import { pancakeRouterAbi } from '../contracts/pancakeRouter.js';
23
+ import { fourmemeMmRouterAbi } from '../contracts/fourmemeMmRouter.js';
24
+ import { computeVolumeSlippage } from '../lib/slippage.js';
25
+ import { assertSupportedToken, TokenNotFoundError, UnsupportedTokenError, } from '../lib/guards.js';
26
+ import { resolveFourmmPassword } from '../lib/env.js';
27
+ import { resolveTradingPath } from '../lib/routing.js';
28
+ import { trackInBackground } from '../lib/tracker.js';
29
+ import { getPublicClient } from '../lib/viem.js';
30
+ import { decryptPrivateKey, getGroup } from '../wallets/groups/store.js';
31
+ import { makeWalletClient } from '../lib/walletClient.js';
32
+ export const trade = Cli.create('trade', {
33
+ description: 'Execute or simulate trades (buy, sell, sniper, batch)',
34
+ })
35
+ // ============================================================
36
+ // fourmm trade buy
37
+ // ============================================================
38
+ .command('buy', {
39
+ description: 'Batch buy tokens for every wallet in a group. Uses TokenManager2.buyTokenAMAP on the bonding curve.',
40
+ options: z.object({
41
+ group: z
42
+ .coerce.number()
43
+ .int()
44
+ .positive()
45
+ .describe('Wallet group ID'),
46
+ token: z
47
+ .string()
48
+ .regex(/^0x[a-fA-F0-9]{40}$/, 'Expected a 40-hex-char 0x-prefixed address')
49
+ .describe('Token contract address'),
50
+ amount: z
51
+ .coerce.number()
52
+ .positive()
53
+ .describe('BNB amount to spend per wallet'),
54
+ slippage: z
55
+ .coerce.number()
56
+ .int()
57
+ .min(1)
58
+ .max(5_000)
59
+ .default(300)
60
+ .describe('Max slippage in basis points (300 = 3%, max 5000)'),
61
+ dryRun: z
62
+ .boolean()
63
+ .default(false)
64
+ .describe('Simulate without broadcasting'),
65
+ password: z
66
+ .string()
67
+ .optional()
68
+ .describe('In-house master password (or FOURMM_PASSWORD env)'),
69
+ }),
70
+ examples: [
71
+ {
72
+ options: {
73
+ group: 1,
74
+ token: '0x802CF8e2673f619c486a2950feE3D24f8A074444',
75
+ amount: 0.01,
76
+ },
77
+ description: 'Dry-run: simulate 0.01 BNB buy from every wallet in group 1',
78
+ },
79
+ {
80
+ options: {
81
+ group: 1,
82
+ token: '0x802CF8e2673f619c486a2950feE3D24f8A074444',
83
+ amount: 0.05,
84
+ slippage: 500,
85
+ },
86
+ description: 'Simulate with 5% slippage',
87
+ },
88
+ ],
89
+ output: z.object({
90
+ token: z.string(),
91
+ variant: z.string(),
92
+ path: z.string(),
93
+ tokenManager: z.string(),
94
+ tradeable: z.boolean(),
95
+ group: z.number(),
96
+ walletCount: z.number(),
97
+ amountPerWallet: z.string(),
98
+ totalSpend: z.string(),
99
+ slippageBps: z.number(),
100
+ dryRun: z.boolean(),
101
+ estimate: z.object({
102
+ estimatedTokens: z.string(),
103
+ estimatedCost: z.string(),
104
+ estimatedFee: z.string(),
105
+ minAmountOut: z.string(),
106
+ }),
107
+ wallets: z.array(z.string()),
108
+ warnings: z.array(z.string()),
109
+ /** Per-wallet broadcast results (empty for dry-run) */
110
+ results: z.array(z.object({
111
+ wallet: z.string(),
112
+ status: z.string(),
113
+ txHash: z.string().optional(),
114
+ error: z.string().optional(),
115
+ })),
116
+ }),
117
+ async run(c) {
118
+ if (!isAddress(c.options.token)) {
119
+ return c.error({
120
+ code: 'INVALID_ADDRESS',
121
+ message: `"${c.options.token}" is not a valid address`,
122
+ });
123
+ }
124
+ const ca = getAddress(c.options.token);
125
+ const password = resolveFourmmPassword(c.options.password);
126
+ if (!password) {
127
+ return c.error({
128
+ code: 'NO_PASSWORD',
129
+ message: 'fourMM master password required (--password or FOURMM_PASSWORD env)',
130
+ });
131
+ }
132
+ // ---- Resolve group ----
133
+ const group = getGroup(password, c.options.group);
134
+ if (!group) {
135
+ return c.error({
136
+ code: 'GROUP_NOT_FOUND',
137
+ message: `Group ${c.options.group} does not exist`,
138
+ });
139
+ }
140
+ if (group.wallets.length === 0) {
141
+ return c.error({
142
+ code: 'EMPTY_GROUP',
143
+ message: `Group ${c.options.group} has no wallets`,
144
+ });
145
+ }
146
+ // ---- Core invariants ----
147
+ let supported;
148
+ try {
149
+ supported = await assertSupportedToken(ca);
150
+ }
151
+ catch (err) {
152
+ if (err instanceof UnsupportedTokenError) {
153
+ return c.error({
154
+ code: 'UNSUPPORTED_TOKEN',
155
+ message: err.message,
156
+ });
157
+ }
158
+ if (err instanceof TokenNotFoundError) {
159
+ return c.error({
160
+ code: 'TOKEN_NOT_FOUND',
161
+ message: err.message,
162
+ });
163
+ }
164
+ return c.error({
165
+ code: 'IDENTIFY_FAILED',
166
+ message: err instanceof Error ? err.message : String(err),
167
+ });
168
+ }
169
+ const client = getPublicClient();
170
+ // resolveTradingPath now returns the full Helper3.getTokenInfo tuple
171
+ // so we can reuse fields (launchTime, etc.) without a second RPC call.
172
+ let routing;
173
+ try {
174
+ routing = await resolveTradingPath(client, ca);
175
+ }
176
+ catch (err) {
177
+ return c.error({
178
+ code: 'ROUTING_FAILED',
179
+ message: err instanceof Error ? err.message : String(err),
180
+ });
181
+ }
182
+ const tradingPath = routing.tradingPath;
183
+ const tokenInfo = routing.rawInfo;
184
+ // ---- Verify launch has happened (reuses rawInfo from resolveTradingPath) ----
185
+ const launchTime = tokenInfo[6];
186
+ const nowUnix = BigInt(Math.floor(Date.now() / 1000));
187
+ const tradeable = launchTime > 0n && launchTime <= nowUnix;
188
+ // Graduated tokens are always tradeable
189
+ if (!tradeable && tradingPath.path !== 'pancake') {
190
+ return c.error({
191
+ code: 'NOT_TRADEABLE',
192
+ message: launchTime === 0n
193
+ ? `Token ${ca} has no launchTime set — protocol would revert on buy`
194
+ : `Token ${ca} launches at ${new Date(Number(launchTime) * 1000).toISOString()} — too early to trade`,
195
+ });
196
+ }
197
+ // ---- Estimate tokens out ----
198
+ const perWalletBnbWei = parseEther(c.options.amount.toString());
199
+ let estimate;
200
+ const slippageBps = BigInt(c.options.slippage);
201
+ if (tradingPath.path === 'pancake') {
202
+ // PancakeSwap: use getAmountsOut for estimation
203
+ try {
204
+ const amounts = await client.readContract({
205
+ address: PANCAKE_V2_ROUTER,
206
+ abi: pancakeRouterAbi,
207
+ functionName: 'getAmountsOut',
208
+ args: [perWalletBnbWei, [WBNB, ca]],
209
+ });
210
+ const estimatedAmount = amounts[1];
211
+ const minAmountOut = (estimatedAmount * (10000n - slippageBps)) / 10000n;
212
+ estimate = {
213
+ estimatedTokens: formatUnits(estimatedAmount, 18),
214
+ estimatedCost: `${c.options.amount} BNB`,
215
+ estimatedFee: '0 BNB',
216
+ minAmountOut: formatUnits(minAmountOut, 18),
217
+ };
218
+ }
219
+ catch (err) {
220
+ return c.error({
221
+ code: 'ESTIMATE_FAILED',
222
+ message: err instanceof Error
223
+ ? `PancakeSwap getAmountsOut failed: ${err.message}`
224
+ : 'getAmountsOut failed',
225
+ });
226
+ }
227
+ }
228
+ else {
229
+ // Bonding curve: use Helper3.tryBuy
230
+ try {
231
+ const result = await client.readContract({
232
+ address: TOKEN_MANAGER_HELPER3,
233
+ abi: tokenManagerHelper3Abi,
234
+ functionName: 'tryBuy',
235
+ args: [ca, 0n, perWalletBnbWei],
236
+ });
237
+ const [, , estimatedAmount, estimatedCost, estimatedFee] = result;
238
+ const minAmountOut = (estimatedAmount * (10000n - slippageBps)) / 10000n;
239
+ estimate = {
240
+ estimatedTokens: formatUnits(estimatedAmount, 18),
241
+ estimatedCost: `${formatEther(estimatedCost)} BNB`,
242
+ estimatedFee: `${formatEther(estimatedFee)} BNB`,
243
+ minAmountOut: formatUnits(minAmountOut, 18),
244
+ };
245
+ }
246
+ catch (err) {
247
+ return c.error({
248
+ code: 'TRYBUY_FAILED',
249
+ message: err instanceof Error
250
+ ? `tryBuy(${ca}, ${c.options.amount} BNB) failed: ${err.message}`
251
+ : 'tryBuy failed',
252
+ });
253
+ }
254
+ }
255
+ const totalSpend = c.options.amount * group.wallets.length;
256
+ // Surface warnings the agent should see before proceeding.
257
+ const warnings = [];
258
+ if (supported.variant === 'anti-sniper-fee') {
259
+ warnings.push('anti-sniper-fee token: dynamic fee decreases block-by-block after launch. ' +
260
+ 'First few blocks can charge 10–20% — consider raising --slippage to 1000+ bps.');
261
+ }
262
+ if (supported.source === 'fallback-network') {
263
+ warnings.push('Four.meme API was unreachable; variant classification defaulted to standard. ' +
264
+ 'If the token is actually TaxToken or X Mode, the downstream RPC layer will still catch it.');
265
+ }
266
+ // ---- Dry run: return estimates only ----
267
+ if (c.options.dryRun) {
268
+ return c.ok({
269
+ token: ca,
270
+ variant: supported.variant,
271
+ path: tradingPath.path,
272
+ tokenManager: tradingPath.router,
273
+ tradeable,
274
+ group: c.options.group,
275
+ walletCount: group.wallets.length,
276
+ amountPerWallet: `${c.options.amount} BNB`,
277
+ totalSpend: `${totalSpend} BNB`,
278
+ slippageBps: c.options.slippage,
279
+ dryRun: true,
280
+ estimate,
281
+ wallets: group.wallets.map((w) => w.address),
282
+ warnings,
283
+ results: [],
284
+ }, {
285
+ cta: {
286
+ commands: [
287
+ {
288
+ command: 'trade buy',
289
+ options: {
290
+ group: c.options.group,
291
+ token: ca,
292
+ amount: c.options.amount,
293
+ },
294
+ description: 'Execute this buy for real (remove --dry-run)',
295
+ },
296
+ ],
297
+ },
298
+ });
299
+ }
300
+ // ---- Live: sign + send buy for each wallet ----
301
+ const config = loadConfig();
302
+ const results = [];
303
+ // Track the latest confirmed block so subsequent readContract calls
304
+ // read post-buy state (RPC "latest" can lag 1-2 blocks behind).
305
+ let lastConfirmedBlock;
306
+ const walletCount = group.wallets.length;
307
+ for (let wi = 0; wi < walletCount; wi++) {
308
+ const w = group.wallets[wi];
309
+ try {
310
+ const pk = decryptPrivateKey(w, password);
311
+ const account = privateKeyToAccount(pk);
312
+ const wc = makeWalletClient(account, config);
313
+ let hash;
314
+ const readAt = lastConfirmedBlock ? { blockNumber: lastConfirmedBlock } : {};
315
+ if (tradingPath.path === 'pancake') {
316
+ const amounts = await client.readContract({
317
+ address: PANCAKE_V2_ROUTER,
318
+ abi: pancakeRouterAbi,
319
+ functionName: 'getAmountsOut',
320
+ args: [perWalletBnbWei, [WBNB, ca]],
321
+ ...readAt,
322
+ });
323
+ const minAmount = (amounts[1] * (10000n - slippageBps)) / 10000n;
324
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
325
+ hash = await wc.writeContract({
326
+ address: PANCAKE_V2_ROUTER,
327
+ abi: pancakeRouterAbi,
328
+ functionName: 'swapExactETHForTokens',
329
+ args: [minAmount, [WBNB, ca], account.address, deadline],
330
+ value: perWalletBnbWei,
331
+ chain: wc.chain,
332
+ account,
333
+ });
334
+ }
335
+ else {
336
+ const tryResult = await client.readContract({
337
+ address: TOKEN_MANAGER_HELPER3,
338
+ abi: tokenManagerHelper3Abi,
339
+ functionName: 'tryBuy',
340
+ args: [ca, 0n, perWalletBnbWei],
341
+ ...readAt,
342
+ });
343
+ const minAmount = (tryResult[2] * (10000n - slippageBps)) / 10000n;
344
+ hash = await wc.writeContract({
345
+ address: tradingPath.router,
346
+ abi: tokenManager2Abi,
347
+ functionName: 'buyTokenAMAP',
348
+ args: [ca, perWalletBnbWei, minAmount],
349
+ value: perWalletBnbWei,
350
+ chain: wc.chain,
351
+ account,
352
+ });
353
+ }
354
+ // Always wait for receipt — report confirmed/failed, not just broadcast
355
+ let receipt;
356
+ let status = 'broadcast';
357
+ try {
358
+ receipt = await client.waitForTransactionReceipt({ hash: hash, timeout: 30_000 });
359
+ lastConfirmedBlock = receipt.blockNumber;
360
+ status = receipt.status === 'success' ? 'confirmed' : 'failed (reverted)';
361
+ }
362
+ catch {
363
+ status = 'broadcast (receipt timeout)';
364
+ }
365
+ trackInBackground(client, hash, {
366
+ ca,
367
+ groupId: c.options.group,
368
+ walletAddress: account.address,
369
+ txType: 'buy',
370
+ knownAmountBnb: -c.options.amount,
371
+ }, 30_000, receipt);
372
+ results.push({ wallet: w.address, status, txHash: hash });
373
+ }
374
+ catch (err) {
375
+ results.push({
376
+ wallet: w.address,
377
+ status: 'failed',
378
+ error: err instanceof Error ? err.message : String(err),
379
+ });
380
+ }
381
+ }
382
+ const successCount = results.filter((r) => r.status === 'broadcast').length;
383
+ return c.ok({
384
+ token: ca,
385
+ variant: supported.variant,
386
+ path: tradingPath.path,
387
+ tokenManager: tradingPath.router,
388
+ tradeable,
389
+ group: c.options.group,
390
+ walletCount: group.wallets.length,
391
+ amountPerWallet: `${c.options.amount} BNB`,
392
+ totalSpend: `${totalSpend} BNB`,
393
+ slippageBps: c.options.slippage,
394
+ dryRun: false,
395
+ estimate,
396
+ wallets: group.wallets.map((w) => w.address),
397
+ warnings,
398
+ results,
399
+ }, {
400
+ cta: {
401
+ commands: [
402
+ {
403
+ command: 'query monitor',
404
+ options: { group: c.options.group, token: ca },
405
+ description: `${successCount}/${group.wallets.length} buys broadcast — check holdings`,
406
+ },
407
+ {
408
+ command: 'trade sell',
409
+ options: {
410
+ group: c.options.group,
411
+ token: ca,
412
+ amount: 'all',
413
+ },
414
+ description: 'Sell everything when ready',
415
+ },
416
+ ],
417
+ },
418
+ });
419
+ },
420
+ })
421
+ // ============================================================
422
+ // fourmm trade sell
423
+ // ============================================================
424
+ .command('sell', {
425
+ description: 'Batch sell tokens from every wallet in a group. Supports amount modes: all / NN% / fixed number.',
426
+ options: z.object({
427
+ group: z
428
+ .coerce.number()
429
+ .int()
430
+ .positive()
431
+ .describe('Wallet group ID'),
432
+ token: z
433
+ .string()
434
+ .regex(/^0x[a-fA-F0-9]{40}$/)
435
+ .describe('Token contract address'),
436
+ amount: z
437
+ .string()
438
+ .default('all')
439
+ .describe('Sell amount: "all", "50%", or a fixed token count'),
440
+ slippage: z
441
+ .coerce.number()
442
+ .int()
443
+ .min(1)
444
+ .max(5_000)
445
+ .default(300)
446
+ .describe('Max slippage in basis points'),
447
+ dryRun: z
448
+ .boolean()
449
+ .default(false)
450
+ .describe('Simulate without broadcasting'),
451
+ password: z
452
+ .string()
453
+ .optional()
454
+ .describe('In-house master password (or FOURMM_PASSWORD env)'),
455
+ }),
456
+ examples: [
457
+ {
458
+ options: {
459
+ group: 1,
460
+ token: '0x802CF8e2673f619c486a2950feE3D24f8A074444',
461
+ amount: 'all',
462
+ dryRun: true,
463
+ },
464
+ description: 'Dry-run: sell all tokens from group 1',
465
+ },
466
+ {
467
+ options: {
468
+ group: 1,
469
+ token: '0x802CF8e2673f619c486a2950feE3D24f8A074444',
470
+ amount: '50%',
471
+ },
472
+ description: 'Sell 50% of holdings',
473
+ },
474
+ ],
475
+ output: z.object({
476
+ token: z.string(),
477
+ group: z.number(),
478
+ mode: z.string(),
479
+ dryRun: z.boolean(),
480
+ walletCount: z.number(),
481
+ results: z.array(z.object({
482
+ wallet: z.string(),
483
+ tokenBalance: z.string(),
484
+ sellAmount: z.string(),
485
+ status: z.string(),
486
+ txHash: z.string().optional(),
487
+ error: z.string().optional(),
488
+ })),
489
+ }),
490
+ async run(c) {
491
+ if (!isAddress(c.options.token)) {
492
+ return c.error({
493
+ code: 'INVALID_ADDRESS',
494
+ message: `"${c.options.token}" is not a valid address`,
495
+ });
496
+ }
497
+ const ca = getAddress(c.options.token);
498
+ const password = resolveFourmmPassword(c.options.password);
499
+ if (!password) {
500
+ return c.error({
501
+ code: 'NO_PASSWORD',
502
+ message: 'fourMM master password required',
503
+ });
504
+ }
505
+ const group = getGroup(password, c.options.group);
506
+ if (!group || group.wallets.length === 0) {
507
+ return c.error({
508
+ code: 'GROUP_NOT_FOUND',
509
+ message: `Group ${c.options.group} does not exist or is empty`,
510
+ });
511
+ }
512
+ // Core invariants
513
+ try {
514
+ await assertSupportedToken(ca);
515
+ }
516
+ catch (err) {
517
+ if (err instanceof UnsupportedTokenError || err instanceof TokenNotFoundError) {
518
+ return c.error({ code: err.code, message: err.message });
519
+ }
520
+ return c.error({ code: 'IDENTIFY_FAILED', message: String(err) });
521
+ }
522
+ const client = getPublicClient();
523
+ let routing;
524
+ try {
525
+ routing = await resolveTradingPath(client, ca);
526
+ }
527
+ catch (err) {
528
+ return c.error({
529
+ code: 'ROUTING_FAILED',
530
+ message: err instanceof Error ? err.message : String(err),
531
+ });
532
+ }
533
+ const isPancake = routing.tradingPath.path === 'pancake';
534
+ // Parse amount mode
535
+ const amountRaw = c.options.amount;
536
+ const isAll = amountRaw === 'all';
537
+ const isPercent = amountRaw.endsWith('%');
538
+ const pctValue = isPercent ? Number(amountRaw.replace('%', '')) : 0;
539
+ if (isPercent && (pctValue <= 0 || pctValue > 100)) {
540
+ return c.error({
541
+ code: 'INVALID_AMOUNT',
542
+ message: `Percentage must be 1-100, got ${amountRaw}`,
543
+ });
544
+ }
545
+ const config = loadConfig();
546
+ // Read token decimals once (Four.meme tokens are 18, but we don't assume)
547
+ let tokenDecimals = 18;
548
+ try {
549
+ const d = await client.readContract({ address: ca, abi: erc20Abi, functionName: 'decimals' });
550
+ tokenDecimals = Number(d);
551
+ }
552
+ catch { /* fall back to 18 */ }
553
+ const results = [];
554
+ let lastConfirmedBlock;
555
+ for (const w of group.wallets) {
556
+ try {
557
+ // Read token balance at confirmed block for sequential consistency
558
+ const readAt = lastConfirmedBlock ? { blockNumber: lastConfirmedBlock } : {};
559
+ const balance = await client.readContract({
560
+ address: ca,
561
+ abi: erc20Abi,
562
+ functionName: 'balanceOf',
563
+ args: [w.address],
564
+ ...readAt,
565
+ });
566
+ if (balance === 0n) {
567
+ results.push({
568
+ wallet: w.address,
569
+ tokenBalance: '0',
570
+ sellAmount: '0',
571
+ status: 'no-balance',
572
+ });
573
+ continue;
574
+ }
575
+ // Compute sell amount
576
+ let sellAmount;
577
+ if (isAll) {
578
+ sellAmount = balance;
579
+ }
580
+ else if (isPercent) {
581
+ sellAmount = (balance * BigInt(Math.round(pctValue * 100))) / 10000n;
582
+ }
583
+ else {
584
+ // Fixed amount: validate it's a number before parsing
585
+ const numVal = Number(amountRaw);
586
+ if (Number.isNaN(numVal) || numVal <= 0) {
587
+ return c.error({
588
+ code: 'INVALID_AMOUNT',
589
+ message: `Expected a positive number, "all", or "NN%", got "${amountRaw}"`,
590
+ });
591
+ }
592
+ sellAmount = parseUnits(amountRaw, tokenDecimals);
593
+ if (sellAmount > balance) {
594
+ results.push({
595
+ wallet: w.address,
596
+ tokenBalance: formatUnits(balance, tokenDecimals),
597
+ sellAmount: formatUnits(sellAmount, tokenDecimals),
598
+ status: 'insufficient-balance',
599
+ });
600
+ continue;
601
+ }
602
+ }
603
+ if (sellAmount === 0n) {
604
+ results.push({
605
+ wallet: w.address,
606
+ tokenBalance: formatUnits(balance, tokenDecimals),
607
+ sellAmount: '0',
608
+ status: 'nothing-to-sell',
609
+ });
610
+ continue;
611
+ }
612
+ if (c.options.dryRun) {
613
+ // Estimate via trySell (bonding curve) or getAmountsOut (pancake)
614
+ try {
615
+ if (isPancake) {
616
+ const amounts = await client.readContract({
617
+ address: PANCAKE_V2_ROUTER,
618
+ abi: pancakeRouterAbi,
619
+ functionName: 'getAmountsOut',
620
+ args: [sellAmount, [ca, WBNB]],
621
+ });
622
+ results.push({
623
+ wallet: w.address,
624
+ tokenBalance: formatUnits(balance, tokenDecimals),
625
+ sellAmount: formatUnits(sellAmount, tokenDecimals),
626
+ status: `ready (est: ${formatEther(amounts[1])} BNB via PancakeSwap)`,
627
+ });
628
+ }
629
+ else {
630
+ const est = await client.readContract({
631
+ address: TOKEN_MANAGER_HELPER3,
632
+ abi: tokenManagerHelper3Abi,
633
+ functionName: 'trySell',
634
+ args: [ca, sellAmount],
635
+ });
636
+ results.push({
637
+ wallet: w.address,
638
+ tokenBalance: formatUnits(balance, tokenDecimals),
639
+ sellAmount: formatUnits(sellAmount, tokenDecimals),
640
+ status: `ready (est: ${formatEther(est[2])} BNB, fee: ${formatEther(est[3])} BNB)`,
641
+ });
642
+ }
643
+ }
644
+ catch {
645
+ results.push({
646
+ wallet: w.address,
647
+ tokenBalance: formatUnits(balance, tokenDecimals),
648
+ sellAmount: formatUnits(sellAmount, tokenDecimals),
649
+ status: 'ready (estimate unavailable)',
650
+ });
651
+ }
652
+ continue;
653
+ }
654
+ // Live: approve + sell
655
+ const pk = decryptPrivateKey(w, password);
656
+ const account = privateKeyToAccount(pk);
657
+ const wc = makeWalletClient(account, config);
658
+ const sellTarget = isPancake ? PANCAKE_V2_ROUTER : routing.tradingPath.router;
659
+ // Approve — MUST wait for confirmation before selling, otherwise
660
+ // the sell tx can land before the approve and revert.
661
+ const approveHash = await wc.writeContract({
662
+ address: ca,
663
+ abi: erc20Abi,
664
+ functionName: 'approve',
665
+ args: [sellTarget, sellAmount],
666
+ chain: wc.chain,
667
+ account,
668
+ });
669
+ await client.waitForTransactionReceipt({
670
+ hash: approveHash,
671
+ timeout: 30_000,
672
+ });
673
+ let hash;
674
+ if (isPancake) {
675
+ // PancakeSwap: swapExactTokensForETHSupportingFeeOnTransferTokens
676
+ const amounts = await client.readContract({
677
+ address: PANCAKE_V2_ROUTER,
678
+ abi: pancakeRouterAbi,
679
+ functionName: 'getAmountsOut',
680
+ args: [sellAmount, [ca, WBNB]],
681
+ ...readAt,
682
+ });
683
+ const minBnbOut = (amounts[1] * (10000n - BigInt(c.options.slippage))) / 10000n;
684
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
685
+ hash = await wc.writeContract({
686
+ address: PANCAKE_V2_ROUTER,
687
+ abi: pancakeRouterAbi,
688
+ functionName: 'swapExactTokensForETHSupportingFeeOnTransferTokens',
689
+ args: [sellAmount, minBnbOut, [ca, WBNB], account.address, deadline],
690
+ chain: wc.chain,
691
+ account,
692
+ });
693
+ }
694
+ else {
695
+ // Bonding curve: TokenManager2.sellToken
696
+ hash = await wc.writeContract({
697
+ address: sellTarget,
698
+ abi: tokenManager2Abi,
699
+ functionName: 'sellToken',
700
+ args: [ca, sellAmount],
701
+ chain: wc.chain,
702
+ account,
703
+ });
704
+ }
705
+ // Post-sell verification: wait for receipt then check if balance changed.
706
+ let sellVerified = 'broadcast';
707
+ let sellReceipt;
708
+ try {
709
+ sellReceipt = await client.waitForTransactionReceipt({ hash: hash, timeout: 30_000 });
710
+ lastConfirmedBlock = sellReceipt.blockNumber;
711
+ const postBalance = await client.readContract({
712
+ address: ca, abi: erc20Abi, functionName: 'balanceOf', args: [w.address],
713
+ });
714
+ if (postBalance === balance) {
715
+ sellVerified = 'WARNING: tx succeeded but token balance unchanged — possible protocol cooldown after buy. Try again in a few blocks.';
716
+ }
717
+ else {
718
+ sellVerified = 'confirmed';
719
+ }
720
+ }
721
+ catch {
722
+ sellVerified = 'broadcast (verification timeout)';
723
+ }
724
+ // Pass pre-fetched receipt so tracker doesn't re-wait
725
+ trackInBackground(client, hash, {
726
+ ca,
727
+ groupId: c.options.group,
728
+ walletAddress: account.address,
729
+ txType: 'sell',
730
+ }, 30_000, sellReceipt);
731
+ results.push({
732
+ wallet: w.address,
733
+ tokenBalance: formatUnits(balance, tokenDecimals),
734
+ sellAmount: formatUnits(sellAmount, tokenDecimals),
735
+ status: sellVerified,
736
+ txHash: hash,
737
+ });
738
+ }
739
+ catch (err) {
740
+ results.push({
741
+ wallet: w.address,
742
+ tokenBalance: '?',
743
+ sellAmount: '?',
744
+ status: 'failed',
745
+ error: err instanceof Error ? err.message : String(err),
746
+ });
747
+ }
748
+ }
749
+ return c.ok({
750
+ token: ca,
751
+ group: c.options.group,
752
+ mode: amountRaw,
753
+ dryRun: c.options.dryRun,
754
+ walletCount: group.wallets.length,
755
+ results,
756
+ }, {
757
+ cta: {
758
+ commands: [
759
+ {
760
+ command: 'query monitor',
761
+ options: { group: c.options.group, token: ca },
762
+ description: 'Check updated holdings + PnL',
763
+ },
764
+ {
765
+ command: 'transfer in',
766
+ options: {
767
+ to: '<your-address>',
768
+ fromGroup: c.options.group,
769
+ amount: 'all',
770
+ },
771
+ description: 'Collect remaining BNB',
772
+ },
773
+ ],
774
+ },
775
+ });
776
+ },
777
+ })
778
+ // ============================================================
779
+ // fourmm trade sniper
780
+ // ============================================================
781
+ .command('sniper', {
782
+ description: 'Sniper buy: each wallet uses its own BNB amount. Count must match wallet count.',
783
+ options: z.object({
784
+ group: z.coerce.number().int().positive().describe('Wallet group ID'),
785
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Token CA'),
786
+ amounts: z.string().describe('Comma-separated BNB amounts per wallet'),
787
+ slippage: z.coerce.number().int().min(1).max(5_000).default(500),
788
+ dryRun: z.boolean().default(false),
789
+ password: z.string().optional(),
790
+ }),
791
+ output: z.object({
792
+ token: z.string(), group: z.number(), dryRun: z.boolean(),
793
+ results: z.array(z.object({ wallet: z.string(), amount: z.string(), status: z.string(), txHash: z.string().optional(), error: z.string().optional() })),
794
+ }),
795
+ async run(c) {
796
+ const ca = getAddress(c.options.token);
797
+ const password = resolveFourmmPassword(c.options.password);
798
+ if (!password)
799
+ return c.error({ code: 'NO_PASSWORD', message: 'Password required' });
800
+ const group = getGroup(password, c.options.group);
801
+ if (!group || group.wallets.length === 0)
802
+ return c.error({ code: 'GROUP_NOT_FOUND', message: 'Group not found or empty' });
803
+ const amountStrs = c.options.amounts.split(',').map((s) => s.trim());
804
+ if (amountStrs.length !== group.wallets.length)
805
+ return c.error({ code: 'AMOUNT_MISMATCH', message: `${amountStrs.length} amounts for ${group.wallets.length} wallets` });
806
+ const amounts = amountStrs.map(Number);
807
+ if (amounts.some((a) => Number.isNaN(a) || a <= 0))
808
+ return c.error({ code: 'INVALID_AMOUNT', message: 'All amounts must be positive' });
809
+ try {
810
+ await assertSupportedToken(ca);
811
+ }
812
+ catch (err) {
813
+ return c.error({ code: err.code ?? 'UNSUPPORTED', message: err.message });
814
+ }
815
+ const client = getPublicClient();
816
+ let routing;
817
+ try {
818
+ routing = await resolveTradingPath(client, ca);
819
+ }
820
+ catch (err) {
821
+ return c.error({ code: 'ROUTING_FAILED', message: err.message });
822
+ }
823
+ if (routing.tradingPath.path === 'pancake')
824
+ return c.error({ code: 'PANCAKE_NOT_YET', message: 'Graduated tokens not supported for sniper' });
825
+ const config = loadConfig();
826
+ const slippageBps = BigInt(c.options.slippage);
827
+ const results = [];
828
+ let lastConfirmedBlock;
829
+ for (let i = 0; i < group.wallets.length; i++) {
830
+ const w = group.wallets[i];
831
+ const bnb = amounts[i];
832
+ const bnbWei = parseEther(bnb.toString());
833
+ if (c.options.dryRun) {
834
+ results.push({ wallet: w.address, amount: `${bnb} BNB`, status: 'ready' });
835
+ continue;
836
+ }
837
+ try {
838
+ const pk = decryptPrivateKey(w, password);
839
+ const account = privateKeyToAccount(pk);
840
+ const wc = makeWalletClient(account, config);
841
+ // Read at the confirmed block so tryBuy sees post-buy state
842
+ const readAt = lastConfirmedBlock ? { blockNumber: lastConfirmedBlock } : {};
843
+ const tryR = await client.readContract({ address: TOKEN_MANAGER_HELPER3, abi: tokenManagerHelper3Abi, functionName: 'tryBuy', args: [ca, 0n, bnbWei], ...readAt });
844
+ const min = (tryR[2] * (10000n - slippageBps)) / 10000n;
845
+ const hash = await wc.writeContract({ address: routing.tradingPath.router, abi: tokenManager2Abi, functionName: 'buyTokenAMAP', args: [ca, bnbWei, min], value: bnbWei, chain: wc.chain, account });
846
+ let receipt;
847
+ let status = 'broadcast';
848
+ try {
849
+ receipt = await client.waitForTransactionReceipt({ hash: hash, timeout: 30_000 });
850
+ lastConfirmedBlock = receipt.blockNumber;
851
+ status = receipt.status === 'success' ? 'confirmed' : 'failed (reverted)';
852
+ }
853
+ catch {
854
+ status = 'broadcast (receipt timeout)';
855
+ }
856
+ trackInBackground(client, hash, { ca, groupId: c.options.group, walletAddress: account.address, txType: 'buy', knownAmountBnb: -bnb }, 30_000, receipt);
857
+ results.push({ wallet: w.address, amount: `${bnb} BNB`, status, txHash: hash });
858
+ }
859
+ catch (err) {
860
+ results.push({ wallet: w.address, amount: `${bnb} BNB`, status: 'failed', error: err.message });
861
+ }
862
+ }
863
+ return c.ok({ token: ca, group: c.options.group, dryRun: c.options.dryRun, results }, { cta: { commands: [{ command: 'query monitor', options: { group: c.options.group, token: ca }, description: 'Check holdings' }] } });
864
+ },
865
+ })
866
+ // ============================================================
867
+ // fourmm trade batch (Router.volume — atomic buy+sell)
868
+ // ============================================================
869
+ .command('batch', {
870
+ description: 'Atomic buy+sell via Router. Each wallet does a round-trip in one tx (zero net position).',
871
+ options: z.object({
872
+ group: z.coerce.number().int().positive().describe('Wallet group ID'),
873
+ token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Token CA'),
874
+ amount: z.coerce.number().positive().describe('BNB per round-trip'),
875
+ slippage: z.coerce.number().int().min(1).max(5_000).default(300).describe('Max slippage bps'),
876
+ dryRun: z.boolean().default(false),
877
+ password: z.string().optional(),
878
+ }),
879
+ output: z.object({
880
+ token: z.string(), group: z.number(), dryRun: z.boolean(), router: z.string(),
881
+ slippageBps: z.number(),
882
+ results: z.array(z.object({ wallet: z.string(), status: z.string(), txHash: z.string().optional(), error: z.string().optional() })),
883
+ }),
884
+ async run(c) {
885
+ const ca = getAddress(c.options.token);
886
+ const password = resolveFourmmPassword(c.options.password);
887
+ if (!password)
888
+ return c.error({ code: 'NO_PASSWORD', message: 'Password required' });
889
+ const group = getGroup(password, c.options.group);
890
+ if (!group || group.wallets.length === 0)
891
+ return c.error({ code: 'GROUP_NOT_FOUND', message: 'Group not found or empty' });
892
+ try {
893
+ await assertSupportedToken(ca);
894
+ }
895
+ catch (err) {
896
+ return c.error({ code: err.code ?? 'UNSUPPORTED', message: err.message });
897
+ }
898
+ const client = getPublicClient();
899
+ const routerAddr = requireRouter();
900
+ const config = loadConfig();
901
+ const bnbWei = parseEther(c.options.amount.toString());
902
+ let routing;
903
+ try {
904
+ routing = await resolveTradingPath(client, ca);
905
+ }
906
+ catch (err) {
907
+ return c.error({ code: 'ROUTING_FAILED', message: err.message });
908
+ }
909
+ const routerFn = routing.tradingPath.path === 'pancake' ? 'volumePancake' : 'volume';
910
+ // Compute slippage-protected minimums ONCE (pure function of token + amount)
911
+ const { minTokenOut, minBnbBack } = await computeVolumeSlippage(client, ca, bnbWei, c.options.slippage);
912
+ const results = [];
913
+ for (const w of group.wallets) {
914
+ if (c.options.dryRun) {
915
+ results.push({ wallet: w.address, status: 'ready' });
916
+ continue;
917
+ }
918
+ try {
919
+ const pk = decryptPrivateKey(w, password);
920
+ const account = privateKeyToAccount(pk);
921
+ const wc = makeWalletClient(account, config);
922
+ const hash = await wc.writeContract({ address: routerAddr, abi: fourmemeMmRouterAbi, functionName: routerFn, args: [ca, minTokenOut, minBnbBack], value: bnbWei, chain: wc.chain, account });
923
+ trackInBackground(client, hash, { ca, groupId: c.options.group, walletAddress: account.address, txType: 'volume' });
924
+ results.push({ wallet: w.address, status: 'broadcast', txHash: hash });
925
+ }
926
+ catch (err) {
927
+ results.push({ wallet: w.address, status: 'failed', error: err.message });
928
+ }
929
+ }
930
+ return c.ok({ token: ca, group: c.options.group, dryRun: c.options.dryRun, router: routerAddr, slippageBps: c.options.slippage, results }, { cta: { commands: [{ command: 'tools volume', options: { group: c.options.group, token: ca, rounds: 10 }, description: 'Run volume bot' }] } });
931
+ },
932
+ });
933
+ //# sourceMappingURL=trade.js.map