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,716 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fourmm wallet` command group — manage in-house wallet groups.
|
|
3
|
+
*
|
|
4
|
+
* These wallets live in ~/.fourmm/wallets/, encrypted with a master password,
|
|
5
|
+
* and are designed for batch operations (sniper groups, volume bots).
|
|
6
|
+
*
|
|
7
|
+
* Week 1 scope: create-group, list-groups. The other 9 wallet commands
|
|
8
|
+
* (generate, group-info, add, import, export, etc.) are Week 2.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import { Cli, z } from 'incur';
|
|
12
|
+
import { getAddress, isAddress } from 'viem';
|
|
13
|
+
import { resolveFourmmPassword } from '../lib/env.js';
|
|
14
|
+
import { getDataStore } from '../datastore/index.js';
|
|
15
|
+
import { getTokenPrice } from '../lib/pricing.js';
|
|
16
|
+
import { getPublicClient } from '../lib/viem.js';
|
|
17
|
+
import { encrypt, decrypt } from '../wallets/groups/encrypt.js';
|
|
18
|
+
import { addGeneratedWallets, addWalletFromPrivateKey, createGroup as createGroupInStore, decryptPrivateKey, deleteGroup as deleteGroupFromStore, getGroup, listGroups as listGroupsFromStore, loadStore, removeWalletFromGroup as removeWalletFromStore, } from '../wallets/groups/store.js';
|
|
19
|
+
export const wallet = Cli.create('wallet', {
|
|
20
|
+
description: 'Manage in-house wallet groups (sniper / volume bot wallets)',
|
|
21
|
+
})
|
|
22
|
+
// ============================================================
|
|
23
|
+
// fourmm wallet create-group
|
|
24
|
+
// ============================================================
|
|
25
|
+
.command('create-group', {
|
|
26
|
+
description: 'Create a new wallet group and populate it with freshly-generated wallets.',
|
|
27
|
+
options: z.object({
|
|
28
|
+
name: z.string().min(1).describe('Group name (human-readable label)'),
|
|
29
|
+
count: z
|
|
30
|
+
.coerce.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(100)
|
|
34
|
+
.default(5)
|
|
35
|
+
.describe('Number of wallets to generate (max 100 per group)'),
|
|
36
|
+
note: z.string().optional().describe('Free-form group description'),
|
|
37
|
+
password: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Master password (or set FOURMM_PASSWORD env var)'),
|
|
41
|
+
}),
|
|
42
|
+
examples: [
|
|
43
|
+
{
|
|
44
|
+
options: { name: 'snipers', count: 10, password: '***' },
|
|
45
|
+
description: 'Create a 10-wallet sniper group',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
options: { name: 'volume', count: 20, note: 'volume bot group' },
|
|
49
|
+
description: 'With note, password from env',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
output: z.object({
|
|
53
|
+
groupId: z.number(),
|
|
54
|
+
name: z.string(),
|
|
55
|
+
walletCount: z.number(),
|
|
56
|
+
addresses: z.array(z.string()),
|
|
57
|
+
message: z.string(),
|
|
58
|
+
}),
|
|
59
|
+
run(c) {
|
|
60
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
61
|
+
if (!password) {
|
|
62
|
+
return c.error({
|
|
63
|
+
code: 'NO_PASSWORD',
|
|
64
|
+
message: 'No master password. Pass --password or set FOURMM_PASSWORD env var.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
let group;
|
|
68
|
+
try {
|
|
69
|
+
group = createGroupInStore(password, {
|
|
70
|
+
name: c.options.name,
|
|
71
|
+
count: c.options.count,
|
|
72
|
+
note: c.options.note,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return c.error({
|
|
77
|
+
code: 'WALLET_CREATE_FAILED',
|
|
78
|
+
message: err instanceof Error ? err.message : String(err),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return c.ok({
|
|
82
|
+
groupId: group.groupId,
|
|
83
|
+
name: group.name,
|
|
84
|
+
walletCount: group.wallets.length,
|
|
85
|
+
addresses: group.wallets.map((w) => w.address),
|
|
86
|
+
message: `Created group ${group.groupId} "${group.name}" with ${group.wallets.length} wallets`,
|
|
87
|
+
}, {
|
|
88
|
+
cta: {
|
|
89
|
+
commands: [
|
|
90
|
+
{
|
|
91
|
+
command: 'wallet group-info',
|
|
92
|
+
options: { id: group.groupId },
|
|
93
|
+
description: 'View full group details',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
command: 'transfer out',
|
|
97
|
+
options: {
|
|
98
|
+
from: '<your-wallet-address>',
|
|
99
|
+
toGroup: group.groupId,
|
|
100
|
+
value: 0.1,
|
|
101
|
+
},
|
|
102
|
+
description: `Fund the group (0.1 BNB each)`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
// ============================================================
|
|
110
|
+
// fourmm wallet generate
|
|
111
|
+
// ============================================================
|
|
112
|
+
.command('generate', {
|
|
113
|
+
description: 'Append freshly-generated wallets to an existing group. Private keys are encrypted at rest with the master password.',
|
|
114
|
+
options: z.object({
|
|
115
|
+
group: z
|
|
116
|
+
.coerce.number()
|
|
117
|
+
.int()
|
|
118
|
+
.positive()
|
|
119
|
+
.describe('Target group ID (see `wallet list-groups`)'),
|
|
120
|
+
count: z
|
|
121
|
+
.coerce.number()
|
|
122
|
+
.int()
|
|
123
|
+
.min(1)
|
|
124
|
+
.max(100)
|
|
125
|
+
.default(1)
|
|
126
|
+
.describe('Number of wallets to add (max 100)'),
|
|
127
|
+
password: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Master password (or set FOURMM_PASSWORD env var)'),
|
|
131
|
+
}),
|
|
132
|
+
examples: [
|
|
133
|
+
{
|
|
134
|
+
options: { group: 1, count: 5 },
|
|
135
|
+
description: 'Append 5 wallets to group 1',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
options: { group: 2, count: 20, password: '***' },
|
|
139
|
+
description: 'Append 20 wallets with explicit password',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
output: z.object({
|
|
143
|
+
groupId: z.number(),
|
|
144
|
+
added: z.number(),
|
|
145
|
+
totalAfter: z.number(),
|
|
146
|
+
addresses: z.array(z.string()),
|
|
147
|
+
message: z.string(),
|
|
148
|
+
}),
|
|
149
|
+
run(c) {
|
|
150
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
151
|
+
if (!password) {
|
|
152
|
+
return c.error({
|
|
153
|
+
code: 'NO_PASSWORD',
|
|
154
|
+
message: 'No master password. Pass --password or set FOURMM_PASSWORD env var.',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Validate group exists up front for a clean error
|
|
158
|
+
const existing = getGroup(password, c.options.group);
|
|
159
|
+
if (!existing) {
|
|
160
|
+
return c.error({
|
|
161
|
+
code: 'GROUP_NOT_FOUND',
|
|
162
|
+
message: `Group ${c.options.group} does not exist. Run \`fourmm wallet create-group\` first.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
let added;
|
|
166
|
+
try {
|
|
167
|
+
added = addGeneratedWallets(password, c.options.group, c.options.count);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
return c.error({
|
|
171
|
+
code: 'WALLET_GENERATE_FAILED',
|
|
172
|
+
message: err instanceof Error ? err.message : String(err),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const totalAfter = existing.wallets.length + added.length;
|
|
176
|
+
return c.ok({
|
|
177
|
+
groupId: c.options.group,
|
|
178
|
+
added: added.length,
|
|
179
|
+
totalAfter,
|
|
180
|
+
addresses: added.map((w) => w.address),
|
|
181
|
+
message: `Added ${added.length} wallets to group ${c.options.group} (total ${totalAfter})`,
|
|
182
|
+
}, {
|
|
183
|
+
cta: {
|
|
184
|
+
commands: [
|
|
185
|
+
{
|
|
186
|
+
command: 'wallet group-info',
|
|
187
|
+
options: { id: c.options.group },
|
|
188
|
+
description: 'Inspect the updated group',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
// ============================================================
|
|
196
|
+
// fourmm wallet add
|
|
197
|
+
// ============================================================
|
|
198
|
+
.command('add', {
|
|
199
|
+
description: 'Import an existing private key into a wallet group. Validates via viem and dedupes by address.',
|
|
200
|
+
options: z.object({
|
|
201
|
+
group: z
|
|
202
|
+
.coerce.number()
|
|
203
|
+
.int()
|
|
204
|
+
.positive()
|
|
205
|
+
.describe('Target group ID'),
|
|
206
|
+
privateKey: z
|
|
207
|
+
.string()
|
|
208
|
+
.regex(/^(0x)?[0-9a-fA-F]{64}$/, 'Expected a 32-byte hex private key')
|
|
209
|
+
.describe('Hex private key (with or without 0x prefix)'),
|
|
210
|
+
note: z
|
|
211
|
+
.string()
|
|
212
|
+
.optional()
|
|
213
|
+
.describe('Free-form note attached to this wallet'),
|
|
214
|
+
password: z
|
|
215
|
+
.string()
|
|
216
|
+
.optional()
|
|
217
|
+
.describe('Master password (or set FOURMM_PASSWORD env var)'),
|
|
218
|
+
}),
|
|
219
|
+
examples: [
|
|
220
|
+
{
|
|
221
|
+
options: {
|
|
222
|
+
group: 1,
|
|
223
|
+
privateKey: '0x0000000000000000000000000000000000000000000000000000000000000001',
|
|
224
|
+
note: 'main wallet',
|
|
225
|
+
},
|
|
226
|
+
description: 'Import a private key into group 1',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
output: z.object({
|
|
230
|
+
groupId: z.number(),
|
|
231
|
+
address: z.string(),
|
|
232
|
+
note: z.string(),
|
|
233
|
+
message: z.string(),
|
|
234
|
+
}),
|
|
235
|
+
outputPolicy: 'all',
|
|
236
|
+
run(c) {
|
|
237
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
238
|
+
if (!password) {
|
|
239
|
+
return c.error({
|
|
240
|
+
code: 'NO_PASSWORD',
|
|
241
|
+
message: 'No master password. Pass --password or set FOURMM_PASSWORD env var.',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const existing = getGroup(password, c.options.group);
|
|
245
|
+
if (!existing) {
|
|
246
|
+
return c.error({
|
|
247
|
+
code: 'GROUP_NOT_FOUND',
|
|
248
|
+
message: `Group ${c.options.group} does not exist`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// Normalise to 0x prefix for viem
|
|
252
|
+
const rawKey = c.options.privateKey.startsWith('0x')
|
|
253
|
+
? c.options.privateKey
|
|
254
|
+
: `0x${c.options.privateKey}`;
|
|
255
|
+
let stored;
|
|
256
|
+
try {
|
|
257
|
+
stored = addWalletFromPrivateKey(password, c.options.group, rawKey, c.options.note ?? '');
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return c.error({
|
|
261
|
+
code: 'WALLET_ADD_FAILED',
|
|
262
|
+
message: err instanceof Error ? err.message : String(err),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return c.ok({
|
|
266
|
+
groupId: c.options.group,
|
|
267
|
+
address: stored.address,
|
|
268
|
+
note: stored.note,
|
|
269
|
+
message: `Added wallet ${stored.address} to group ${c.options.group}`,
|
|
270
|
+
}, {
|
|
271
|
+
cta: {
|
|
272
|
+
commands: [
|
|
273
|
+
{
|
|
274
|
+
command: 'wallet group-info',
|
|
275
|
+
options: { id: c.options.group },
|
|
276
|
+
description: 'Verify the group now includes this wallet',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
command: 'query balance',
|
|
280
|
+
options: { address: stored.address },
|
|
281
|
+
description: 'Check the new wallet balance',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
// ============================================================
|
|
289
|
+
// fourmm wallet group-info
|
|
290
|
+
// ============================================================
|
|
291
|
+
.command('group-info', {
|
|
292
|
+
description: 'Show all wallets in a group. --show-keys decrypts private keys (dangerous — never pipe to a file in an unsafe environment).',
|
|
293
|
+
options: z.object({
|
|
294
|
+
id: z
|
|
295
|
+
.coerce.number()
|
|
296
|
+
.int()
|
|
297
|
+
.positive()
|
|
298
|
+
.describe('Group ID'),
|
|
299
|
+
showKeys: z
|
|
300
|
+
.boolean()
|
|
301
|
+
.default(false)
|
|
302
|
+
.describe('Include decrypted private keys (outputs are suppressed for human TTY)'),
|
|
303
|
+
password: z
|
|
304
|
+
.string()
|
|
305
|
+
.optional()
|
|
306
|
+
.describe('Master password (or set FOURMM_PASSWORD env var)'),
|
|
307
|
+
}),
|
|
308
|
+
examples: [
|
|
309
|
+
{
|
|
310
|
+
options: { id: 1 },
|
|
311
|
+
description: 'Show wallet addresses for group 1',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
options: { id: 1, showKeys: true },
|
|
315
|
+
description: 'Also reveal private keys (agent-only output)',
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
output: z.object({
|
|
319
|
+
groupId: z.number(),
|
|
320
|
+
name: z.string(),
|
|
321
|
+
note: z.string(),
|
|
322
|
+
walletCount: z.number(),
|
|
323
|
+
createdAt: z.string(),
|
|
324
|
+
updatedAt: z.string(),
|
|
325
|
+
wallets: z.array(z.object({
|
|
326
|
+
address: z.string(),
|
|
327
|
+
note: z.string(),
|
|
328
|
+
privateKey: z.string().optional(),
|
|
329
|
+
})),
|
|
330
|
+
}),
|
|
331
|
+
run(c) {
|
|
332
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
333
|
+
if (!password) {
|
|
334
|
+
return c.error({
|
|
335
|
+
code: 'NO_PASSWORD',
|
|
336
|
+
message: 'No master password. Pass --password or set FOURMM_PASSWORD env var.',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Security: refuse to print private keys to a human TTY.
|
|
340
|
+
// incur's c.agent is `true` when stdout is not a TTY (piped / MCP /
|
|
341
|
+
// agent context), `false` on interactive terminals. `outputPolicy`
|
|
342
|
+
// can't be made conditional on an option, so we enforce it here.
|
|
343
|
+
if (c.options.showKeys && !c.agent) {
|
|
344
|
+
return c.error({
|
|
345
|
+
code: 'REFUSE_SHOW_KEYS_TTY',
|
|
346
|
+
message: 'Refusing to print private keys to a terminal. Pipe through an ' +
|
|
347
|
+
'agent (e.g. `... --format json | your-tool`) or run this from an ' +
|
|
348
|
+
'MCP / skill context where stdout is not a TTY.',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
let group;
|
|
352
|
+
try {
|
|
353
|
+
group = getGroup(password, c.options.id);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
return c.error({
|
|
357
|
+
code: 'WALLET_READ_FAILED',
|
|
358
|
+
message: err instanceof Error ? err.message : String(err),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (!group) {
|
|
362
|
+
return c.error({
|
|
363
|
+
code: 'GROUP_NOT_FOUND',
|
|
364
|
+
message: `Group ${c.options.id} does not exist`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const wallets = group.wallets.map((w) => {
|
|
368
|
+
const row = {
|
|
369
|
+
address: w.address,
|
|
370
|
+
note: w.note,
|
|
371
|
+
};
|
|
372
|
+
if (c.options.showKeys) {
|
|
373
|
+
try {
|
|
374
|
+
row.privateKey = decryptPrivateKey(w, password);
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
row.privateKey = '<decrypt failed>';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return row;
|
|
381
|
+
});
|
|
382
|
+
return c.ok({
|
|
383
|
+
groupId: group.groupId,
|
|
384
|
+
name: group.name,
|
|
385
|
+
note: group.note,
|
|
386
|
+
walletCount: group.wallets.length,
|
|
387
|
+
createdAt: group.createdAt,
|
|
388
|
+
updatedAt: group.updatedAt,
|
|
389
|
+
wallets,
|
|
390
|
+
}, {
|
|
391
|
+
cta: {
|
|
392
|
+
commands: [
|
|
393
|
+
{
|
|
394
|
+
command: 'transfer out',
|
|
395
|
+
options: { from: '<your-wallet-address>', toGroup: group.groupId, value: 0.1 },
|
|
396
|
+
description: 'Fund this group',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
command: 'query balance',
|
|
400
|
+
options: { address: group.wallets[0]?.address ?? '' },
|
|
401
|
+
description: 'Check a wallet balance',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
// ============================================================
|
|
409
|
+
// fourmm wallet list-groups
|
|
410
|
+
// ============================================================
|
|
411
|
+
.command('list-groups', {
|
|
412
|
+
description: 'List all wallet groups in the in-house vault.',
|
|
413
|
+
options: z.object({
|
|
414
|
+
password: z
|
|
415
|
+
.string()
|
|
416
|
+
.optional()
|
|
417
|
+
.describe('Master password (or set FOURMM_PASSWORD env var)'),
|
|
418
|
+
}),
|
|
419
|
+
examples: [{ description: 'List every wallet group' }],
|
|
420
|
+
output: z.object({
|
|
421
|
+
count: z.number(),
|
|
422
|
+
groups: z.array(z.object({
|
|
423
|
+
groupId: z.number(),
|
|
424
|
+
name: z.string(),
|
|
425
|
+
note: z.string(),
|
|
426
|
+
walletCount: z.number(),
|
|
427
|
+
createdAt: z.string(),
|
|
428
|
+
updatedAt: z.string(),
|
|
429
|
+
})),
|
|
430
|
+
}),
|
|
431
|
+
run(c) {
|
|
432
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
433
|
+
if (!password) {
|
|
434
|
+
return c.error({
|
|
435
|
+
code: 'NO_PASSWORD',
|
|
436
|
+
message: 'No master password. Pass --password or set FOURMM_PASSWORD env var.',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
let groups;
|
|
440
|
+
try {
|
|
441
|
+
groups = listGroupsFromStore(password);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
return c.error({
|
|
445
|
+
code: 'WALLET_LIST_FAILED',
|
|
446
|
+
message: err instanceof Error ? err.message : String(err),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return c.ok({
|
|
450
|
+
count: groups.length,
|
|
451
|
+
groups,
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
// ============================================================
|
|
456
|
+
// fourmm wallet delete-group
|
|
457
|
+
// ============================================================
|
|
458
|
+
.command('delete-group', {
|
|
459
|
+
description: 'Delete an entire wallet group from the store.',
|
|
460
|
+
options: z.object({
|
|
461
|
+
id: z.coerce.number().int().positive().describe('Group ID to delete'),
|
|
462
|
+
force: z.boolean().default(false).describe('Skip confirmation (required for non-interactive)'),
|
|
463
|
+
password: z.string().optional().describe('Master password (or FOURMM_PASSWORD env)'),
|
|
464
|
+
}),
|
|
465
|
+
output: z.object({ groupId: z.number(), message: z.string() }),
|
|
466
|
+
run(c) {
|
|
467
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
468
|
+
if (!password)
|
|
469
|
+
return c.error({ code: 'NO_PASSWORD', message: 'No master password.' });
|
|
470
|
+
const group = getGroup(password, c.options.id);
|
|
471
|
+
if (!group)
|
|
472
|
+
return c.error({ code: 'GROUP_NOT_FOUND', message: `Group ${c.options.id} does not exist` });
|
|
473
|
+
if (!c.options.force) {
|
|
474
|
+
return c.error({ code: 'CONFIRM_REQUIRED', message: `Group ${c.options.id} "${group.name}" has ${group.wallets.length} wallets. Pass --force to confirm deletion.` });
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
deleteGroupFromStore(password, c.options.id);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
return c.error({ code: 'DELETE_FAILED', message: err instanceof Error ? err.message : String(err) });
|
|
481
|
+
}
|
|
482
|
+
return c.ok({ groupId: c.options.id, message: `Deleted group ${c.options.id} "${group.name}"` }, { cta: { commands: [{ command: 'wallet list-groups', options: {}, description: 'View remaining groups' }] } });
|
|
483
|
+
},
|
|
484
|
+
})
|
|
485
|
+
// ============================================================
|
|
486
|
+
// fourmm wallet remove
|
|
487
|
+
// ============================================================
|
|
488
|
+
.command('remove', {
|
|
489
|
+
description: 'Remove a single wallet from a group by address.',
|
|
490
|
+
options: z.object({
|
|
491
|
+
group: z.coerce.number().int().positive().describe('Group ID'),
|
|
492
|
+
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Expected 0x-prefixed address').describe('Wallet address to remove'),
|
|
493
|
+
password: z.string().optional().describe('Master password (or FOURMM_PASSWORD env)'),
|
|
494
|
+
}),
|
|
495
|
+
output: z.object({ groupId: z.number(), address: z.string(), message: z.string() }),
|
|
496
|
+
run(c) {
|
|
497
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
498
|
+
if (!password)
|
|
499
|
+
return c.error({ code: 'NO_PASSWORD', message: 'No master password.' });
|
|
500
|
+
const addr = getAddress(c.options.address);
|
|
501
|
+
try {
|
|
502
|
+
removeWalletFromStore(password, c.options.group, addr);
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
return c.error({ code: 'REMOVE_FAILED', message: err instanceof Error ? err.message : String(err) });
|
|
506
|
+
}
|
|
507
|
+
return c.ok({ groupId: c.options.group, address: addr, message: `Removed ${addr} from group ${c.options.group}` }, { cta: { commands: [{ command: 'wallet group-info', options: { id: c.options.group }, description: 'Verify wallet removed' }] } });
|
|
508
|
+
},
|
|
509
|
+
})
|
|
510
|
+
// ============================================================
|
|
511
|
+
// fourmm wallet import
|
|
512
|
+
// ============================================================
|
|
513
|
+
.command('import', {
|
|
514
|
+
description: 'Import wallets from a CSV file (address,privateKey,note per line) into a group.',
|
|
515
|
+
options: z.object({
|
|
516
|
+
group: z.coerce.number().int().positive().describe('Target group ID'),
|
|
517
|
+
file: z.string().describe('Path to CSV file'),
|
|
518
|
+
password: z.string().optional().describe('Master password (or FOURMM_PASSWORD env)'),
|
|
519
|
+
}),
|
|
520
|
+
output: z.object({ groupId: z.number(), imported: z.number(), skipped: z.number(), skippedAddresses: z.array(z.string()), message: z.string() }),
|
|
521
|
+
run(c) {
|
|
522
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
523
|
+
if (!password)
|
|
524
|
+
return c.error({ code: 'NO_PASSWORD', message: 'No master password.' });
|
|
525
|
+
const group = getGroup(password, c.options.group);
|
|
526
|
+
if (!group)
|
|
527
|
+
return c.error({ code: 'GROUP_NOT_FOUND', message: `Group ${c.options.group} does not exist` });
|
|
528
|
+
if (!fs.existsSync(c.options.file)) {
|
|
529
|
+
return c.error({ code: 'FILE_NOT_FOUND', message: `File not found: ${c.options.file}` });
|
|
530
|
+
}
|
|
531
|
+
const raw = fs.readFileSync(c.options.file, 'utf-8');
|
|
532
|
+
const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
533
|
+
let imported = 0;
|
|
534
|
+
let skipped = 0;
|
|
535
|
+
const skippedAddresses = [];
|
|
536
|
+
for (const line of lines) {
|
|
537
|
+
// Skip header row
|
|
538
|
+
if (line.toLowerCase().startsWith('address,'))
|
|
539
|
+
continue;
|
|
540
|
+
const parts = line.split(',').map((p) => p.trim());
|
|
541
|
+
const address = parts[0] ?? '';
|
|
542
|
+
const pk = parts[1];
|
|
543
|
+
const note = parts[2] ?? '';
|
|
544
|
+
if (!pk || !pk.match(/^(0x)?[0-9a-fA-F]{64}$/)) {
|
|
545
|
+
skipped++;
|
|
546
|
+
skippedAddresses.push(address || `line:${lines.indexOf(line) + 1}`);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const rawKey = pk.startsWith('0x') ? pk : `0x${pk}`;
|
|
550
|
+
try {
|
|
551
|
+
addWalletFromPrivateKey(password, c.options.group, rawKey, note);
|
|
552
|
+
imported++;
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
skipped++;
|
|
556
|
+
skippedAddresses.push(address || `line:${lines.indexOf(line) + 1}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return c.ok({ groupId: c.options.group, imported, skipped, skippedAddresses, message: `Imported ${imported} wallets, skipped ${skipped}${skippedAddresses.length > 0 ? ` (${skippedAddresses.join(', ')})` : ''}` }, { cta: { commands: [{ command: 'wallet group-info', options: { id: c.options.group }, description: 'View updated group' }] } });
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
// ============================================================
|
|
563
|
+
// fourmm wallet export
|
|
564
|
+
// ============================================================
|
|
565
|
+
.command('export', {
|
|
566
|
+
description: 'Export wallets from a group to a CSV file (address,privateKey,note).',
|
|
567
|
+
options: z.object({
|
|
568
|
+
group: z.coerce.number().int().positive().describe('Source group ID'),
|
|
569
|
+
file: z.string().describe('Output CSV path'),
|
|
570
|
+
password: z.string().optional().describe('Master password (or FOURMM_PASSWORD env)'),
|
|
571
|
+
}),
|
|
572
|
+
output: z.object({ groupId: z.number(), count: z.number(), file: z.string(), decryptFailures: z.number(), message: z.string() }),
|
|
573
|
+
run(c) {
|
|
574
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
575
|
+
if (!password)
|
|
576
|
+
return c.error({ code: 'NO_PASSWORD', message: 'No master password.' });
|
|
577
|
+
const group = getGroup(password, c.options.group);
|
|
578
|
+
if (!group)
|
|
579
|
+
return c.error({ code: 'GROUP_NOT_FOUND', message: `Group ${c.options.group} does not exist` });
|
|
580
|
+
const rows = ['address,privateKey,note'];
|
|
581
|
+
let decryptFailures = 0;
|
|
582
|
+
for (const w of group.wallets) {
|
|
583
|
+
let pk;
|
|
584
|
+
try {
|
|
585
|
+
pk = decryptPrivateKey(w, password);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
pk = '<decrypt-failed>';
|
|
589
|
+
decryptFailures++;
|
|
590
|
+
}
|
|
591
|
+
rows.push(`${w.address},${pk},${w.note.replace(/,/g, ';')}`);
|
|
592
|
+
}
|
|
593
|
+
if (decryptFailures > 0 && decryptFailures === group.wallets.length) {
|
|
594
|
+
return c.error({
|
|
595
|
+
code: 'ALL_DECRYPT_FAILED',
|
|
596
|
+
message: `All ${group.wallets.length} wallets failed to decrypt. Wrong password?`,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
fs.writeFileSync(c.options.file, rows.join('\n') + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
600
|
+
const warnings = decryptFailures > 0
|
|
601
|
+
? `WARNING: ${decryptFailures}/${group.wallets.length} wallets failed to decrypt — their private keys are written as '<decrypt-failed>' in the CSV.`
|
|
602
|
+
: undefined;
|
|
603
|
+
return c.ok({
|
|
604
|
+
groupId: c.options.group,
|
|
605
|
+
count: group.wallets.length,
|
|
606
|
+
file: c.options.file,
|
|
607
|
+
decryptFailures,
|
|
608
|
+
message: warnings ?? `Exported ${group.wallets.length} wallets to ${c.options.file}`,
|
|
609
|
+
}, { cta: { commands: [{ command: 'wallet group-info', options: { id: c.options.group }, description: 'View group' }] } });
|
|
610
|
+
},
|
|
611
|
+
})
|
|
612
|
+
// ============================================================
|
|
613
|
+
// fourmm wallet export-group
|
|
614
|
+
// ============================================================
|
|
615
|
+
.command('export-group', {
|
|
616
|
+
description: 'Export all groups as JSON, optionally AES encrypted.',
|
|
617
|
+
options: z.object({
|
|
618
|
+
file: z.string().describe('Output file path'),
|
|
619
|
+
encrypt: z.boolean().default(false).describe('AES-encrypt the output with master password'),
|
|
620
|
+
password: z.string().optional().describe('Master password (or FOURMM_PASSWORD env)'),
|
|
621
|
+
}),
|
|
622
|
+
output: z.object({ groupCount: z.number(), encrypted: z.boolean(), file: z.string(), message: z.string() }),
|
|
623
|
+
run(c) {
|
|
624
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
625
|
+
if (!password)
|
|
626
|
+
return c.error({ code: 'NO_PASSWORD', message: 'No master password.' });
|
|
627
|
+
const store = loadStore(password);
|
|
628
|
+
if (!store)
|
|
629
|
+
return c.error({ code: 'NO_STORE', message: 'No wallet store found.' });
|
|
630
|
+
const groupCount = Object.keys(store.groups).length;
|
|
631
|
+
// Strip passwordCheck from the export to prevent brute-force attacks.
|
|
632
|
+
// The passwordCheck is a known-plaintext AES marker that, combined with
|
|
633
|
+
// encrypted private keys, is a self-contained brute-force target.
|
|
634
|
+
const exportData = { ...store, passwordCheck: undefined };
|
|
635
|
+
const json = JSON.stringify(exportData, null, 2);
|
|
636
|
+
if (c.options.encrypt) {
|
|
637
|
+
const encrypted = encrypt(json, password);
|
|
638
|
+
fs.writeFileSync(c.options.file, encrypted, { encoding: 'utf-8', mode: 0o600 });
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
fs.writeFileSync(c.options.file, json, { encoding: 'utf-8', mode: 0o600 });
|
|
642
|
+
}
|
|
643
|
+
return c.ok({ groupCount, encrypted: c.options.encrypt, file: c.options.file, message: `Exported ${groupCount} groups to ${c.options.file}${c.options.encrypt ? ' (encrypted)' : ''}` }, { cta: { commands: [{ command: 'wallet list-groups', options: {}, description: 'View groups' }] } });
|
|
644
|
+
},
|
|
645
|
+
})
|
|
646
|
+
// ============================================================
|
|
647
|
+
// fourmm wallet overview
|
|
648
|
+
// ============================================================
|
|
649
|
+
.command('overview', {
|
|
650
|
+
description: 'Aggregate PnL across wallet groups from DataStore.',
|
|
651
|
+
options: z.object({
|
|
652
|
+
groups: z.string().describe('Comma-separated group IDs (e.g. "1,2")'),
|
|
653
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional().describe('Filter by token CA'),
|
|
654
|
+
password: z.string().optional().describe('Master password (or FOURMM_PASSWORD env)'),
|
|
655
|
+
}),
|
|
656
|
+
output: z.object({
|
|
657
|
+
groupIds: z.array(z.number()),
|
|
658
|
+
tokenFilter: z.string(),
|
|
659
|
+
totalRealizedPnl: z.number(),
|
|
660
|
+
totalUnrealizedPnl: z.number(),
|
|
661
|
+
totalValueBnb: z.number(),
|
|
662
|
+
groups: z.array(z.object({
|
|
663
|
+
groupId: z.number(), realizedPnl: z.number(), unrealizedPnl: z.number(), valueBnb: z.number(),
|
|
664
|
+
})),
|
|
665
|
+
}),
|
|
666
|
+
async run(c) {
|
|
667
|
+
const password = resolveFourmmPassword(c.options.password);
|
|
668
|
+
if (!password)
|
|
669
|
+
return c.error({ code: 'NO_PASSWORD', message: 'No master password.' });
|
|
670
|
+
const groupIds = c.options.groups.split(',').map((s) => Number.parseInt(s.trim(), 10)).filter((n) => Number.isInteger(n) && n > 0);
|
|
671
|
+
if (groupIds.length === 0)
|
|
672
|
+
return c.error({ code: 'NO_GROUPS', message: 'Provide at least one group ID' });
|
|
673
|
+
const ds = getDataStore();
|
|
674
|
+
const client = getPublicClient();
|
|
675
|
+
const tokens = c.options.token ? [getAddress(c.options.token)] : ds.listTokens();
|
|
676
|
+
let totalRealized = 0;
|
|
677
|
+
let totalUnrealized = 0;
|
|
678
|
+
let totalValue = 0;
|
|
679
|
+
const groupSummaries = [];
|
|
680
|
+
for (const gid of groupIds) {
|
|
681
|
+
let gRealized = 0;
|
|
682
|
+
let gUnrealized = 0;
|
|
683
|
+
let gValue = 0;
|
|
684
|
+
for (const ca of tokens) {
|
|
685
|
+
let priceBnb = 0;
|
|
686
|
+
try {
|
|
687
|
+
const p = await getTokenPrice(client, ca);
|
|
688
|
+
priceBnb = p.priceBnb;
|
|
689
|
+
}
|
|
690
|
+
catch { /* skip */ }
|
|
691
|
+
const holdings = ds.getHoldings(ca, gid);
|
|
692
|
+
if (!holdings)
|
|
693
|
+
continue;
|
|
694
|
+
for (const w of holdings.wallets) {
|
|
695
|
+
const currentVal = w.tokenBalance * priceBnb;
|
|
696
|
+
const unrealized = currentVal - w.tokenBalance * w.avgBuyPrice;
|
|
697
|
+
gRealized += w.realizedPnl;
|
|
698
|
+
gUnrealized += unrealized;
|
|
699
|
+
gValue += currentVal;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
totalRealized += gRealized;
|
|
703
|
+
totalUnrealized += gUnrealized;
|
|
704
|
+
totalValue += gValue;
|
|
705
|
+
groupSummaries.push({ groupId: gid, realizedPnl: gRealized, unrealizedPnl: gUnrealized, valueBnb: gValue });
|
|
706
|
+
}
|
|
707
|
+
return c.ok({
|
|
708
|
+
groupIds, tokenFilter: c.options.token ?? 'all',
|
|
709
|
+
totalRealizedPnl: totalRealized, totalUnrealizedPnl: totalUnrealized, totalValueBnb: totalValue,
|
|
710
|
+
groups: groupSummaries,
|
|
711
|
+
}, { cta: { commands: [
|
|
712
|
+
{ command: 'query transactions', options: { group: groupIds[0] }, description: 'View transaction history' },
|
|
713
|
+
] } });
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
//# sourceMappingURL=wallet.js.map
|