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.
- package/README.md +147 -0
- package/dist/bin.d.ts +9 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +14 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +319 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +35 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +145 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/query.d.ts +51 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +364 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/token.d.ts +55 -0
- package/dist/commands/token.d.ts.map +1 -0
- package/dist/commands/token.js +650 -0
- package/dist/commands/token.js.map +1 -0
- package/dist/commands/tools.d.ts +54 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +499 -0
- package/dist/commands/tools.js.map +1 -0
- package/dist/commands/trade.d.ts +63 -0
- package/dist/commands/trade.d.ts.map +1 -0
- package/dist/commands/trade.js +933 -0
- package/dist/commands/trade.js.map +1 -0
- package/dist/commands/transfer.d.ts +51 -0
- package/dist/commands/transfer.d.ts.map +1 -0
- package/dist/commands/transfer.js +728 -0
- package/dist/commands/transfer.js.map +1 -0
- package/dist/commands/wallet.d.ts +111 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +716 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/contracts/erc20.d.ts +72 -0
- package/dist/contracts/erc20.d.ts.map +1 -0
- package/dist/contracts/erc20.js +55 -0
- package/dist/contracts/erc20.js.map +1 -0
- package/dist/contracts/fourmemeMmRouter.d.ts +68 -0
- package/dist/contracts/fourmemeMmRouter.d.ts.map +1 -0
- package/dist/contracts/fourmemeMmRouter.js +48 -0
- package/dist/contracts/fourmemeMmRouter.js.map +1 -0
- package/dist/contracts/pancakeRouter.d.ts +73 -0
- package/dist/contracts/pancakeRouter.d.ts.map +1 -0
- package/dist/contracts/pancakeRouter.js +50 -0
- package/dist/contracts/pancakeRouter.js.map +1 -0
- package/dist/contracts/tokenManager2.d.ts +193 -0
- package/dist/contracts/tokenManager2.d.ts.map +1 -0
- package/dist/contracts/tokenManager2.js +108 -0
- package/dist/contracts/tokenManager2.js.map +1 -0
- package/dist/contracts/tokenManagerHelper3.d.ts +118 -0
- package/dist/contracts/tokenManagerHelper3.d.ts.map +1 -0
- package/dist/contracts/tokenManagerHelper3.js +66 -0
- package/dist/contracts/tokenManagerHelper3.js.map +1 -0
- package/dist/datastore/cache.d.ts +20 -0
- package/dist/datastore/cache.d.ts.map +1 -0
- package/dist/datastore/cache.js +45 -0
- package/dist/datastore/cache.js.map +1 -0
- package/dist/datastore/index.d.ts +85 -0
- package/dist/datastore/index.d.ts.map +1 -0
- package/dist/datastore/index.js +341 -0
- package/dist/datastore/index.js.map +1 -0
- package/dist/datastore/paths.d.ts +17 -0
- package/dist/datastore/paths.d.ts.map +1 -0
- package/dist/datastore/paths.js +39 -0
- package/dist/datastore/paths.js.map +1 -0
- package/dist/datastore/types.d.ts +105 -0
- package/dist/datastore/types.d.ts.map +1 -0
- package/dist/datastore/types.js +8 -0
- package/dist/datastore/types.js.map +1 -0
- package/dist/fourmeme/auth.d.ts +22 -0
- package/dist/fourmeme/auth.d.ts.map +1 -0
- package/dist/fourmeme/auth.js +78 -0
- package/dist/fourmeme/auth.js.map +1 -0
- package/dist/fourmeme/create.d.ts +31 -0
- package/dist/fourmeme/create.d.ts.map +1 -0
- package/dist/fourmeme/create.js +111 -0
- package/dist/fourmeme/create.js.map +1 -0
- package/dist/fourmeme/upload.d.ts +16 -0
- package/dist/fourmeme/upload.d.ts.map +1 -0
- package/dist/fourmeme/upload.js +52 -0
- package/dist/fourmeme/upload.js.map +1 -0
- package/dist/lib/bundle.d.ts +51 -0
- package/dist/lib/bundle.d.ts.map +1 -0
- package/dist/lib/bundle.js +95 -0
- package/dist/lib/bundle.js.map +1 -0
- package/dist/lib/config.d.ts +58 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +183 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/const.d.ts +165 -0
- package/dist/lib/const.d.ts.map +1 -0
- package/dist/lib/const.js +98 -0
- package/dist/lib/const.js.map +1 -0
- package/dist/lib/env.d.ts +14 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +18 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/guards.d.ts +44 -0
- package/dist/lib/guards.d.ts.map +1 -0
- package/dist/lib/guards.js +65 -0
- package/dist/lib/guards.js.map +1 -0
- package/dist/lib/identify.d.ts +85 -0
- package/dist/lib/identify.d.ts.map +1 -0
- package/dist/lib/identify.js +88 -0
- package/dist/lib/identify.js.map +1 -0
- package/dist/lib/pricing.d.ts +62 -0
- package/dist/lib/pricing.d.ts.map +1 -0
- package/dist/lib/pricing.js +302 -0
- package/dist/lib/pricing.js.map +1 -0
- package/dist/lib/routing.d.ts +57 -0
- package/dist/lib/routing.d.ts.map +1 -0
- package/dist/lib/routing.js +67 -0
- package/dist/lib/routing.js.map +1 -0
- package/dist/lib/slippage.d.ts +29 -0
- package/dist/lib/slippage.d.ts.map +1 -0
- package/dist/lib/slippage.js +110 -0
- package/dist/lib/slippage.js.map +1 -0
- package/dist/lib/tracker.d.ts +68 -0
- package/dist/lib/tracker.d.ts.map +1 -0
- package/dist/lib/tracker.js +155 -0
- package/dist/lib/tracker.js.map +1 -0
- package/dist/lib/viem.d.ts +12 -0
- package/dist/lib/viem.d.ts.map +1 -0
- package/dist/lib/viem.js +44 -0
- package/dist/lib/viem.js.map +1 -0
- package/dist/lib/wallet-rows.d.ts +30 -0
- package/dist/lib/wallet-rows.d.ts.map +1 -0
- package/dist/lib/wallet-rows.js +9 -0
- package/dist/lib/wallet-rows.js.map +1 -0
- package/dist/lib/walletClient.d.ts +16 -0
- package/dist/lib/walletClient.d.ts.map +1 -0
- package/dist/lib/walletClient.js +26 -0
- package/dist/lib/walletClient.js.map +1 -0
- package/dist/wallets/groups/encrypt.d.ts +26 -0
- package/dist/wallets/groups/encrypt.d.ts.map +1 -0
- package/dist/wallets/groups/encrypt.js +52 -0
- package/dist/wallets/groups/encrypt.js.map +1 -0
- package/dist/wallets/groups/generate.d.ts +19 -0
- package/dist/wallets/groups/generate.d.ts.map +1 -0
- package/dist/wallets/groups/generate.js +36 -0
- package/dist/wallets/groups/generate.js.map +1 -0
- package/dist/wallets/groups/store.d.ts +107 -0
- package/dist/wallets/groups/store.d.ts.map +1 -0
- package/dist/wallets/groups/store.js +254 -0
- package/dist/wallets/groups/store.js.map +1 -0
- package/package.json +50 -0
- 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
|