@mingderwang/wallet 0.1.1 → 0.1.2

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/dist/index.js CHANGED
@@ -79,6 +79,10 @@ function randomBytesHex(len = 32) {
79
79
  }
80
80
 
81
81
  // src/index.ts
82
+ function processEnvPassword() {
83
+ const v = process.env.WALLET_PASSWORD ?? process.env.XSIFT_WALLET_PASSWORD;
84
+ return v && v.length > 0 ? v : undefined;
85
+ }
82
86
  var NETWORKS = {
83
87
  ethereumMainnet: { networkId: "ethereum-mainnet", chainId: 1 },
84
88
  ethereumSepolia: { networkId: "ethereum-sepolia", chainId: 11155111 },
@@ -162,6 +166,7 @@ export {
162
166
  rpcUrl,
163
167
  readKeystore,
164
168
  randomBytesHex,
169
+ processEnvPassword,
165
170
  loadWallet,
166
171
  importWallet,
167
172
  getBalances,
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
- "name": "@mingderwang/wallet",
3
- "version": "0.1.1",
4
- "description": "Ethereum wallet + encrypted keystore helpers (scrypt + AES-256-GCM) built on ethers v6.",
2
+ "name": "@mingderwang/wallet",
3
+ "version": "0.1.2",
4
+ "description": "Ethereum wallet + encrypted keystore helpers (scrypt + AES-256-GCM) built on ethers v6. Ships a `wallet` CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "wallet": "./dist/cli.js"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "types": "./dist/index.d.ts",
package/src/cli.ts ADDED
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import type { Wallet } from 'ethers';
6
+ import {
7
+ createWallet as generateWallet,
8
+ importWallet,
9
+ loadWallet,
10
+ encryptPrivateKey,
11
+ saveKeystore,
12
+ readKeystore,
13
+ deleteFile,
14
+ getBalances,
15
+ formatAmount,
16
+ USDC,
17
+ processEnvPassword,
18
+ type EncryptedKeystore,
19
+ } from './index';
20
+
21
+ const VERSION = '0.1.2';
22
+
23
+ const C = {
24
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
25
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
26
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
27
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
28
+ cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
29
+ gray: (s: string) => `\x1b[90m${s}\x1b[0m`,
30
+ };
31
+
32
+ const hasEnvPassword = () => typeof processEnvPassword() === 'string';
33
+
34
+ function prompt(question: string): Promise<string> {
35
+ process.stderr.write(question);
36
+ return new Promise((resolve) => {
37
+ const stdin = process.stdin;
38
+ stdin.resume();
39
+ stdin.once('data', (buf: Buffer) => {
40
+ stdin.pause();
41
+ const v = buf.toString().replace(/\r?\n$/, '');
42
+ process.stderr.write('');
43
+ resolve(v);
44
+ });
45
+ });
46
+ }
47
+
48
+ function promptHidden(question: string): Promise<string> {
49
+ if (hasEnvPassword()) {
50
+ process.stderr.write(`${question}[using WALLET_PASSWORD env]\n`);
51
+ return Promise.resolve(processEnvPassword() as string);
52
+ }
53
+ process.stderr.write(question);
54
+ return new Promise((resolve) => {
55
+ const stdin = process.stdin;
56
+ const wasRaw = !!stdin.isRaw;
57
+ stdin.setRawMode(true);
58
+ stdin.resume();
59
+ let input = '';
60
+ const onData = (buf: Buffer) => {
61
+ const code = buf[0];
62
+ if (code === 3) {
63
+ stdin.removeListener('data', onData);
64
+ stdin.setRawMode(false);
65
+ stdin.pause();
66
+ process.stderr.write('\n');
67
+ process.exit(130);
68
+ }
69
+ if (code === 13 || code === 10) {
70
+ stdin.removeListener('data', onData);
71
+ stdin.setRawMode(false);
72
+ stdin.pause();
73
+ process.stderr.write('\n');
74
+ resolve(input);
75
+ return;
76
+ }
77
+ if (code === 127 || code === 8) {
78
+ input = input.slice(0, -1);
79
+ return;
80
+ }
81
+ input += buf.toString('utf8');
82
+ };
83
+ stdin.on('data', onData);
84
+ });
85
+ }
86
+
87
+ function defaultPath(): string {
88
+ return process.env.WALLET_PATH ?? join(homedir(), '.wallet', 'wallet.json');
89
+ }
90
+
91
+ function walletPath(opts: Record<string, string>): string {
92
+ return opts.path ?? defaultPath();
93
+ }
94
+
95
+ function printCreated(wallet: Wallet, path: string): void {
96
+ console.log('');
97
+ console.log(` ${C.bold('Wallet created')}`);
98
+ console.log(' ' + '─'.repeat(46));
99
+ console.log(` ${C.green('Address')} ${wallet.address}`);
100
+ console.log(` ${C.gray('Keystore')} ${path}`);
101
+ console.log('');
102
+ console.log(` ${C.yellow('Keep the password safe — it signs payments.')}`);
103
+ console.log(` ${C.yellow('Back up the keystore file; losing both loses the funds.')}`);
104
+ }
105
+
106
+ async function readPasswordTwice(action: string): Promise<string> {
107
+ for (;;) {
108
+ const p1 = await promptHidden(`New keystore password (${action}): `);
109
+ if (p1.length < 8) {
110
+ console.log(C.yellow('Password must be at least 8 characters.'));
111
+ continue;
112
+ }
113
+ const p2 = await promptHidden('Confirm password: ');
114
+ if (p1 !== p2) {
115
+ console.log(C.red('Passwords do not match. Try again.'));
116
+ continue;
117
+ }
118
+ return p1;
119
+ }
120
+ }
121
+
122
+ async function loadUnlocked(path: string, action: string): Promise<{ wallet: Wallet; keystore: EncryptedKeystore }> {
123
+ if (!existsSync(path)) {
124
+ console.log(C.red('No wallet found at ' + path) + ' — run ' + C.bold('wallet create') + ' first.');
125
+ process.exit(2);
126
+ }
127
+ const keystore = readKeystore(path);
128
+ for (let attempt = 1; ; attempt++) {
129
+ const password = await promptHidden(`Keystore password (${action}): `);
130
+ try {
131
+ const wallet = loadWallet(keystore, password);
132
+ return { wallet, keystore };
133
+ } catch {
134
+ if (attempt >= 3) {
135
+ console.log(C.red('Incorrect password. Exiting.'));
136
+ process.exit(1);
137
+ }
138
+ console.log(C.red('Incorrect password, try again.'));
139
+ }
140
+ }
141
+ }
142
+
143
+ function guardNoWallet(path: string): void {
144
+ if (existsSync(path)) {
145
+ console.log(C.yellow(`A wallet already exists at ${path}`) + ` — delete it with \`wallet delete --path ${path}\` first.`);
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ // ---------- commands ----------
151
+
152
+ async function cmdCreate(opts: Record<string, string>): Promise<void> {
153
+ const path = walletPath(opts);
154
+ guardNoWallet(path);
155
+ const password = await readPasswordTwice('create');
156
+ const { wallet } = generateWallet();
157
+ saveKeystore(encryptPrivateKey(wallet.privateKey, password, wallet.address), path);
158
+ if (opts.json) {
159
+ console.log(JSON.stringify({ address: wallet.address, path }, null, 2));
160
+ return;
161
+ }
162
+ printCreated(wallet, path);
163
+ }
164
+
165
+ async function cmdImport(opts: Record<string, string>): Promise<void> {
166
+ const path = walletPath(opts);
167
+ guardNoWallet(path);
168
+ const secret = opts.secret ?? (await prompt('Enter mnemonic phrase (or 0x private key): ')).trim();
169
+ let wallet: Wallet;
170
+ try {
171
+ ({ wallet } = importWallet(secret));
172
+ } catch {
173
+ console.log(C.red('Invalid mnemonic or private key.'));
174
+ process.exit(1);
175
+ }
176
+ const password = await readPasswordTwice('import');
177
+ saveKeystore(encryptPrivateKey(wallet.privateKey, password, wallet.address), path);
178
+ if (opts.json) {
179
+ console.log(JSON.stringify({ address: wallet.address, path }, null, 2));
180
+ return;
181
+ }
182
+ printCreated(wallet, path);
183
+ }
184
+
185
+ async function cmdAddress(opts: Record<string, string>): Promise<void> {
186
+ const { wallet } = await loadUnlocked(walletPath(opts), 'unlock');
187
+ console.log(wallet.address);
188
+ }
189
+
190
+ async function cmdBalance(opts: Record<string, string>): Promise<void> {
191
+ const { wallet } = await loadUnlocked(walletPath(opts), 'balance');
192
+ const network = opts.network ?? 'ethereum-sepolia';
193
+ try {
194
+ const { eth, usdc } = await getBalances(wallet, network);
195
+ const usdcInfo = USDC[network];
196
+ if (opts.json) {
197
+ console.log(
198
+ JSON.stringify(
199
+ {
200
+ network,
201
+ address: wallet.address,
202
+ eth,
203
+ usdc: usdcInfo
204
+ ? { raw: usdc, formatted: formatAmount(usdc, usdcInfo.decimals), symbol: usdcInfo.symbol }
205
+ : null,
206
+ },
207
+ null,
208
+ 2
209
+ )
210
+ );
211
+ return;
212
+ }
213
+ console.log('');
214
+ console.log(` ${C.bold('Wallet')} ${wallet.address}`);
215
+ console.log(` ${C.bold('Network')} ${network}`);
216
+ console.log(' ' + '─'.repeat(46));
217
+ console.log(` ${C.cyan('ETH')} ${(Number(eth) / 1e18).toFixed(6)}`);
218
+ if (usdcInfo) console.log(` ${C.cyan(usdcInfo.symbol)} ${formatAmount(usdc, usdcInfo.decimals)}`);
219
+ } catch (e) {
220
+ console.log(C.red(`Failed to query ${network}: ${e instanceof Error ? e.message : e}`));
221
+ process.exit(1);
222
+ }
223
+ }
224
+
225
+ async function cmdExport(opts: Record<string, string>): Promise<void> {
226
+ const { wallet } = await loadUnlocked(walletPath(opts), 'export');
227
+ console.log('\n' + C.yellow('Anyone with this private key controls the wallet.'));
228
+ console.log(` Private key: ${C.bold(wallet.privateKey)}`);
229
+ console.log(` Mnemonic : ${wallet.mnemonic?.phrase ?? C.gray('(imported as private key only)')}`);
230
+ }
231
+
232
+ async function cmdDelete(opts: Record<string, string>): Promise<void> {
233
+ const path = walletPath(opts);
234
+ if (!existsSync(path)) {
235
+ console.log(C.red('No wallet found at ' + path));
236
+ process.exit(1);
237
+ }
238
+ const answer = (await prompt(`Delete ${path}? Type ${C.bold('yes')} to confirm: `)).trim();
239
+ if (answer !== 'yes') {
240
+ console.log(C.gray('Aborted.'));
241
+ return;
242
+ }
243
+ deleteFile(path);
244
+ console.log(C.green('Wallet deleted.'));
245
+ }
246
+
247
+ // ---------- help ----------
248
+
249
+ const HELP = `wallet v${VERSION}
250
+
251
+ Manage an encrypted Ethereum keystore (scrypt + AES-256-GCM) at ~/.wallet/wallet.json.
252
+
253
+ Usage:
254
+ wallet create [--path <file>] [--json]
255
+ wallet import <mnemonic | private key> [--path <file>] [--json]
256
+ wallet address [--path <file>]
257
+ wallet balance [--path <file>] [--network <ethereum-sepolia|ethereum-mainnet|base-sepolia|base>]
258
+ wallet export [--path <file>]
259
+ wallet delete [--path <file>]
260
+
261
+ Options:
262
+ --path <file> Keystore file (default ~/.wallet/wallet.json)
263
+ --network <id> Network for balance lookups (default ethereum-sepolia)
264
+ --json Machine-readable output
265
+
266
+ Password:
267
+ Prompts interactively, or set WALLET_PASSWORD in the environment for scripting.`;
268
+
269
+ function printHelp(): void {
270
+ console.log(HELP);
271
+ }
272
+
273
+ // ---------- entry ----------
274
+
275
+ interface Parsed {
276
+ cmd: string;
277
+ positional: string[];
278
+ opts: Record<string, string>;
279
+ }
280
+
281
+ function parseArgs(args: string[]): Parsed {
282
+ const cmd = args.find((a) => !a.startsWith('--')) ?? 'help';
283
+ const positional: string[] = [];
284
+ const opts: Record<string, string> = {};
285
+ for (const a of args) {
286
+ if (a === cmd) continue;
287
+ if (a === '--json') {
288
+ opts.json = '1';
289
+ continue;
290
+ }
291
+ if (a === '--network' || a === '--path') {
292
+ const next = args[args.indexOf(a) + 1];
293
+ if (!next || next.startsWith('--')) {
294
+ console.log(C.red(`Missing value for ${a}`));
295
+ process.exit(1);
296
+ }
297
+ opts[a.slice(2)] = next;
298
+ continue;
299
+ }
300
+ if (!a.startsWith('--')) positional.push(a);
301
+ }
302
+ return { cmd, positional, opts };
303
+ }
304
+
305
+ async function main(): Promise<void> {
306
+ const { cmd, positional, opts } = parseArgs(process.argv.slice(2));
307
+ switch (cmd) {
308
+ case 'create': return cmdCreate(opts);
309
+ case 'import': return cmdImport({ ...opts, secret: positional[0] ?? opts.secret });
310
+ case 'address': return cmdAddress(opts);
311
+ case 'balance': return cmdBalance(opts);
312
+ case 'export': return cmdExport(opts);
313
+ case 'delete': return cmdDelete(opts);
314
+ case 'help':
315
+ case '-h':
316
+ case '--help':
317
+ return printHelp();
318
+ case '-v':
319
+ case '--version':
320
+ return console.log(VERSION);
321
+ default:
322
+ console.log(C.red(`Unknown command: ${cmd}`) + '\n');
323
+ printHelp();
324
+ process.exit(1);
325
+ }
326
+ }
327
+
328
+ main().catch((e) => {
329
+ console.error(e instanceof Error ? e.message : e);
330
+ process.exit(1);
331
+ });
package/src/index.ts CHANGED
@@ -12,6 +12,11 @@ import {
12
12
  export { encryptPrivateKey, decryptPrivateKey, verifyPassword, randomBytesHex };
13
13
  export type { EncryptedKeystore };
14
14
 
15
+ export function processEnvPassword(): string | undefined {
16
+ const v = process.env.WALLET_PASSWORD ?? process.env.XSIFT_WALLET_PASSWORD;
17
+ return v && v.length > 0 ? v : undefined;
18
+ }
19
+
15
20
  export const NETWORKS = {
16
21
  ethereumMainnet: { networkId: 'ethereum-mainnet', chainId: 1 },
17
22
  ethereumSepolia: { networkId: 'ethereum-sepolia', chainId: 11155111 },