hood-cli 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/dist/cli.js ADDED
@@ -0,0 +1,1996 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ MAINNET_ADDRESSES,
4
+ MAINNET_EXPLORER_URL,
5
+ ODYSSEY_ADDRESSES,
6
+ STOCK_TOKEN_DECIMALS,
7
+ StockTokenEligibilityError,
8
+ TESTNET_ADDRESSES,
9
+ TESTNET_STOCK_TOKENS,
10
+ USDG_DECIMALS,
11
+ buildSwapTx,
12
+ createHoodClient,
13
+ ensureApproval,
14
+ erc20Abi,
15
+ formatUsdg,
16
+ getMultiplier,
17
+ getPortfolio,
18
+ getQuote,
19
+ getRecentLaunches,
20
+ getStockToken,
21
+ getStockTokenByAddress,
22
+ getUsdgBalance,
23
+ isStockTokenAddress,
24
+ isStockTokenSymbol,
25
+ listPricedStockTokens,
26
+ listStockTokens,
27
+ odysseyTradedEvent,
28
+ quoteSwap,
29
+ watchLaunches,
30
+ watchTransfers
31
+ } from "./chunk-CWEBZB3L.js";
32
+
33
+ // src/program.ts
34
+ import { Command as Command14 } from "commander";
35
+ import { readFileSync as readFileSync4 } from "fs";
36
+ import { fileURLToPath } from "url";
37
+ import { dirname as dirname3, join as join2 } from "path";
38
+
39
+ // src/commands/price.ts
40
+ import { Command } from "commander";
41
+
42
+ // src/context.ts
43
+ import { privateKeyToAccount } from "viem/accounts";
44
+ import { http, isHex } from "viem";
45
+
46
+ // src/config.ts
47
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
48
+ import { homedir } from "os";
49
+ import { dirname, join } from "path";
50
+
51
+ // src/errors.ts
52
+ var EXIT = {
53
+ OK: 0,
54
+ /** Generic runtime failure. */
55
+ ERROR: 1,
56
+ /** Bad usage / invalid arguments. */
57
+ USAGE: 2,
58
+ /** Network / RPC unreachable or timed out. */
59
+ NETWORK: 3,
60
+ /** A guard rail refused the action (eligibility, spend cap, unconfirmed). */
61
+ GUARD: 4,
62
+ /** The requested resource was not found (unknown symbol, missing tx). */
63
+ NOT_FOUND: 5,
64
+ /** Wallet required but not configured / wrong password. */
65
+ WALLET: 6
66
+ };
67
+ var CliError = class extends Error {
68
+ exitCode;
69
+ hint;
70
+ cause;
71
+ constructor(message, options = {}) {
72
+ super(message);
73
+ this.name = "CliError";
74
+ this.exitCode = options.exitCode ?? EXIT.ERROR;
75
+ this.hint = options.hint;
76
+ this.cause = options.cause;
77
+ }
78
+ };
79
+ var usageError = (m, hint) => new CliError(m, { exitCode: EXIT.USAGE, hint });
80
+ var guardError = (m, hint) => new CliError(m, { exitCode: EXIT.GUARD, hint });
81
+ var notFoundError = (m, hint) => new CliError(m, { exitCode: EXIT.NOT_FOUND, hint });
82
+ var walletError = (m, hint) => new CliError(m, { exitCode: EXIT.WALLET, hint });
83
+ function toCliError(err) {
84
+ if (err instanceof CliError) return err;
85
+ const message = err instanceof Error ? err.message : String(err);
86
+ const name = err instanceof Error ? err.name : "";
87
+ switch (name) {
88
+ case "UnknownSymbolError":
89
+ return new CliError(message, { exitCode: EXIT.NOT_FOUND, cause: err, hint: "Run `hood stocks` to list every Stock Token symbol." });
90
+ case "FeedNotFoundError":
91
+ return new CliError(message, { exitCode: EXIT.NOT_FOUND, cause: err, hint: "This token has no Chainlink feed \u2014 try `hood token <address>` for on-chain data." });
92
+ case "StaleFeedError":
93
+ return new CliError(message, { exitCode: EXIT.ERROR, cause: err, hint: "Stock feeds pause outside market hours (24/5). Pass --max-age to widen the window." });
94
+ case "NoRouteError":
95
+ return new CliError(message, { exitCode: EXIT.NOT_FOUND, cause: err, hint: "No Uniswap v3 pool with liquidity connects these tokens on this network." });
96
+ case "StockTokenEligibilityError":
97
+ return new CliError("Stock Token acquisition is gated.", {
98
+ exitCode: EXIT.GUARD,
99
+ cause: err,
100
+ hint: "Stock Tokens are tokenized securities and may not be sold to US persons. Re-run with --acknowledge-eligibility if you are eligible."
101
+ });
102
+ case "NoAccountError":
103
+ return new CliError("This action needs a wallet.", {
104
+ exitCode: EXIT.WALLET,
105
+ cause: err,
106
+ hint: "Configure one with `hood config set wallet`, or set ROBINHOOD_CHAIN_PRIVATE_KEY."
107
+ });
108
+ case "FeedConnectionError":
109
+ return new CliError(message, { exitCode: EXIT.NETWORK, cause: err });
110
+ }
111
+ if (/fetch failed|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|socket hang up|network|timeout/i.test(message)) {
112
+ return new CliError("Could not reach the Robinhood Chain RPC.", {
113
+ exitCode: EXIT.NETWORK,
114
+ cause: err,
115
+ hint: "Check your connection, or set a custom RPC with `hood config set rpc <url>`."
116
+ });
117
+ }
118
+ return new CliError(message, { exitCode: EXIT.ERROR, cause: err });
119
+ }
120
+
121
+ // src/config.ts
122
+ var CONFIG_KEYS = ["network", "rpc", "testnetRpc", "alchemyKey", "maxSpendUsd"];
123
+ function configDir() {
124
+ return process.env.HOOD_CONFIG_DIR ?? join(homedir(), ".config", "hood");
125
+ }
126
+ function configPath() {
127
+ return join(configDir(), "config.json");
128
+ }
129
+ function defaultKeystorePath() {
130
+ return join(configDir(), "keystore.json");
131
+ }
132
+ function loadConfig() {
133
+ const path = configPath();
134
+ if (!existsSync(path)) return {};
135
+ try {
136
+ return JSON.parse(readFileSync(path, "utf8"));
137
+ } catch (err) {
138
+ throw usageError(`Config at ${path} is not valid JSON.`, "Delete it or fix it by hand.");
139
+ }
140
+ }
141
+ function saveConfig(config) {
142
+ const path = configPath();
143
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
144
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
145
+ }
146
+ function setConfigValue(config, key, value) {
147
+ const next = { ...config };
148
+ switch (key) {
149
+ case "network":
150
+ if (value !== "mainnet" && value !== "testnet") throw usageError('network must be "mainnet" or "testnet".');
151
+ next.network = value;
152
+ break;
153
+ case "rpc":
154
+ next.rpc = value;
155
+ break;
156
+ case "testnetRpc":
157
+ next.testnetRpc = value;
158
+ break;
159
+ case "alchemyKey":
160
+ next.alchemyKey = value;
161
+ break;
162
+ case "maxSpendUsd": {
163
+ const n = Number(value);
164
+ if (!Number.isFinite(n) || n < 0) throw usageError("maxSpendUsd must be a non-negative number.");
165
+ next.maxSpendUsd = n;
166
+ break;
167
+ }
168
+ default:
169
+ throw usageError(`Unknown config key "${key}".`, `Valid keys: ${CONFIG_KEYS.join(", ")}, wallet.`);
170
+ }
171
+ return next;
172
+ }
173
+ function redactedConfig(config) {
174
+ return {
175
+ network: config.network ?? "mainnet",
176
+ rpc: config.rpc ?? null,
177
+ testnetRpc: config.testnetRpc ?? null,
178
+ alchemyKey: config.alchemyKey ? maskKey(config.alchemyKey) : null,
179
+ walletAddress: config.walletAddress ?? null,
180
+ walletKeystore: config.walletKeystore ?? (config.walletAddress ? defaultKeystorePath() : null),
181
+ maxSpendUsd: config.maxSpendUsd ?? null
182
+ };
183
+ }
184
+ function maskKey(key) {
185
+ if (key.length <= 6) return "\u2022\u2022\u2022\u2022";
186
+ return `${key.slice(0, 3)}\u2026${key.slice(-3)}`;
187
+ }
188
+
189
+ // src/keystore.ts
190
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
191
+ import { dirname as dirname2 } from "path";
192
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv, timingSafeEqual } from "crypto";
193
+ var SCRYPT = { N: 1 << 15, r: 8, p: 1, keylen: 32 };
194
+ function deriveKey(password, salt) {
195
+ return scryptSync(password, salt, SCRYPT.keylen, {
196
+ N: SCRYPT.N,
197
+ r: SCRYPT.r,
198
+ p: SCRYPT.p,
199
+ maxmem: 128 * 1024 * 1024
200
+ });
201
+ }
202
+ function writeKeystore(path, privateKey, address, password) {
203
+ if (password.length < 8) throw walletError("Wallet password must be at least 8 characters.");
204
+ const salt = randomBytes(32);
205
+ const iv = randomBytes(12);
206
+ const key = deriveKey(password, salt);
207
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
208
+ const plaintext = Buffer.from(privateKey.replace(/^0x/, ""), "hex");
209
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
210
+ const tag = cipher.getAuthTag();
211
+ const file = {
212
+ version: 1,
213
+ address,
214
+ kdf: "scrypt",
215
+ kdfparams: { ...SCRYPT, salt: salt.toString("hex") },
216
+ cipher: "aes-256-gcm",
217
+ iv: iv.toString("hex"),
218
+ ciphertext: ciphertext.toString("hex"),
219
+ tag: tag.toString("hex")
220
+ };
221
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
222
+ writeFileSync2(path, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
223
+ }
224
+ function decryptKeystore(path, password) {
225
+ const file = readKeystoreFile(path);
226
+ const salt = Buffer.from(file.kdfparams.salt, "hex");
227
+ const key = deriveKey(password, salt);
228
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(file.iv, "hex"));
229
+ decipher.setAuthTag(Buffer.from(file.tag, "hex"));
230
+ try {
231
+ const plaintext = Buffer.concat([
232
+ decipher.update(Buffer.from(file.ciphertext, "hex")),
233
+ decipher.final()
234
+ ]);
235
+ return `0x${plaintext.toString("hex")}`;
236
+ } catch {
237
+ throw walletError("Wrong wallet password.", "The keystore could not be decrypted.");
238
+ }
239
+ }
240
+ function passwordsMatch(a, b) {
241
+ const ba = Buffer.from(a);
242
+ const bb = Buffer.from(b);
243
+ if (ba.length !== bb.length) return false;
244
+ return timingSafeEqual(ba, bb);
245
+ }
246
+ function keystoreExists(path) {
247
+ return existsSync2(path);
248
+ }
249
+ function readKeystoreFile(path) {
250
+ if (!existsSync2(path)) throw walletError(`No keystore at ${path}.`, "Create one with `hood config set wallet`.");
251
+ try {
252
+ const file = JSON.parse(readFileSync2(path, "utf8"));
253
+ if (file.version !== 1 || file.cipher !== "aes-256-gcm") throw new Error("unsupported keystore");
254
+ return file;
255
+ } catch (err) {
256
+ if (err instanceof Error && err.name === "CliError") throw err;
257
+ throw walletError(`Keystore at ${path} is corrupt or unsupported.`);
258
+ }
259
+ }
260
+
261
+ // src/prompt.ts
262
+ import { createInterface } from "readline";
263
+
264
+ // src/ui/ansi.ts
265
+ var colorEnabled = process.env.FORCE_COLOR ? process.env.FORCE_COLOR !== "0" : !!process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
266
+ function setColorEnabled(enabled) {
267
+ colorEnabled = enabled;
268
+ }
269
+ var wrap = (open, close) => (s) => colorEnabled ? `\x1B[${open}m${s}\x1B[${close}m` : String(s);
270
+ var bold = wrap(1, 22);
271
+ var dim = wrap(2, 22);
272
+ var italic = wrap(3, 23);
273
+ var underline = wrap(4, 24);
274
+ var red = wrap(31, 39);
275
+ var green = wrap(32, 39);
276
+ var yellow = wrap(33, 39);
277
+ var blue = wrap(34, 39);
278
+ var magenta = wrap(35, 39);
279
+ var cyan = wrap(36, 39);
280
+ var white = wrap(37, 39);
281
+ var gray = wrap(90, 39);
282
+ function accent(s) {
283
+ return colorEnabled ? `\x1B[38;5;79m${s}\x1B[39m` : s;
284
+ }
285
+ function stripAnsi(s) {
286
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
287
+ }
288
+ function width(s) {
289
+ return stripAnsi(s).length;
290
+ }
291
+ function padEnd(s, len) {
292
+ const w = width(s);
293
+ return w >= len ? s : s + " ".repeat(len - w);
294
+ }
295
+ function padStart(s, len) {
296
+ const w = width(s);
297
+ return w >= len ? s : " ".repeat(len - w) + s;
298
+ }
299
+ function truncate(s, len) {
300
+ if (s.length <= len) return s;
301
+ if (len <= 1) return s.slice(0, len);
302
+ return s.slice(0, len - 1) + "\u2026";
303
+ }
304
+
305
+ // src/prompt.ts
306
+ function canPrompt() {
307
+ return !!process.stdin.isTTY;
308
+ }
309
+ async function confirm(question) {
310
+ if (!canPrompt()) {
311
+ throw guardError("Confirmation required but stdin is not a terminal.", "Re-run in an interactive shell, or pass --yes to skip the prompt.");
312
+ }
313
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
314
+ try {
315
+ const answer = await new Promise((resolve2) => {
316
+ rl.question(`${yellow("?")} ${bold(question)} ${dim("[y/N]")} `, resolve2);
317
+ });
318
+ return /^y(es)?$/i.test(answer.trim());
319
+ } finally {
320
+ rl.close();
321
+ }
322
+ }
323
+ async function readPassword(label = "Wallet password:", envVar = "HOOD_WALLET_PASSWORD") {
324
+ const fromEnv = process.env[envVar];
325
+ if (fromEnv) return fromEnv;
326
+ if (!canPrompt()) {
327
+ throw guardError("A wallet password is required.", `Set ${envVar} or run in an interactive terminal.`);
328
+ }
329
+ return new Promise((resolve2, reject) => {
330
+ const stdin = process.stdin;
331
+ process.stderr.write(`${dim(label)} `);
332
+ stdin.setRawMode?.(true);
333
+ stdin.resume();
334
+ stdin.setEncoding("utf8");
335
+ let value = "";
336
+ const onData = (chunk) => {
337
+ for (const ch of chunk) {
338
+ if (ch === "\n" || ch === "\r" || ch === "") {
339
+ cleanup();
340
+ process.stderr.write("\n");
341
+ resolve2(value);
342
+ return;
343
+ }
344
+ if (ch === "") {
345
+ cleanup();
346
+ process.stderr.write("\n");
347
+ reject(guardError("Aborted."));
348
+ return;
349
+ }
350
+ if (ch === "\x7F" || ch === "\b") {
351
+ value = value.slice(0, -1);
352
+ } else {
353
+ value += ch;
354
+ }
355
+ }
356
+ };
357
+ const cleanup = () => {
358
+ stdin.setRawMode?.(false);
359
+ stdin.pause();
360
+ stdin.removeListener("data", onData);
361
+ };
362
+ stdin.on("data", onData);
363
+ });
364
+ }
365
+
366
+ // src/context.ts
367
+ function resolveRpcUrl(config, network, override) {
368
+ if (override) return override;
369
+ if (network === "testnet") return config.testnetRpc;
370
+ if (config.alchemyKey) return `https://robinhood-mainnet.g.alchemy.com/v2/${config.alchemyKey}`;
371
+ return config.rpc;
372
+ }
373
+ function buildTransport(rpcUrl) {
374
+ return http(rpcUrl, { retryCount: 5, retryDelay: 500, timeout: 2e4 });
375
+ }
376
+ function createContext(opts) {
377
+ const config = loadConfig();
378
+ const network = opts.network ?? config.network ?? "mainnet";
379
+ const rpcUrl = resolveRpcUrl(config, network, opts.rpc);
380
+ let readClient = null;
381
+ let walletClient = null;
382
+ return {
383
+ config,
384
+ network,
385
+ json: !!opts.json,
386
+ verbose: !!opts.verbose,
387
+ assumeYes: !!opts.yes,
388
+ acknowledgeEligibility: !!opts.acknowledgeEligibility,
389
+ rpcUrl,
390
+ read() {
391
+ if (!readClient) {
392
+ readClient = createHoodClient({ chain: network, transport: buildTransport(rpcUrl) });
393
+ }
394
+ return readClient;
395
+ },
396
+ async wallet() {
397
+ if (walletClient) return walletClient;
398
+ const account = await loadAccount(config);
399
+ walletClient = createHoodClient({
400
+ chain: network,
401
+ transport: buildTransport(rpcUrl),
402
+ account,
403
+ acknowledgeStockTokenEligibility: !!opts.acknowledgeEligibility
404
+ });
405
+ return walletClient;
406
+ }
407
+ };
408
+ }
409
+ async function loadAccount(config) {
410
+ const envKey = process.env.ROBINHOOD_CHAIN_PRIVATE_KEY;
411
+ if (envKey) {
412
+ const key = envKey.startsWith("0x") ? envKey : `0x${envKey}`;
413
+ if (!isHex(key) || key.length !== 66) {
414
+ throw walletError("ROBINHOOD_CHAIN_PRIVATE_KEY is not a valid 32-byte hex key.");
415
+ }
416
+ return privateKeyToAccount(key);
417
+ }
418
+ const keystorePath = config.walletKeystore ?? defaultKeystorePath();
419
+ if (!keystoreExists(keystorePath)) {
420
+ throw walletError("No wallet configured.", "Run `hood config set wallet`, or set ROBINHOOD_CHAIN_PRIVATE_KEY.");
421
+ }
422
+ const password = await readPassword();
423
+ const privateKey = decryptKeystore(keystorePath, password);
424
+ return privateKeyToAccount(privateKey);
425
+ }
426
+
427
+ // src/output.ts
428
+ function jsonReplacer(_key, value) {
429
+ return typeof value === "bigint" ? value.toString() : value;
430
+ }
431
+ function printJson(data) {
432
+ process.stdout.write(JSON.stringify(data, jsonReplacer, 2) + "\n");
433
+ }
434
+ function printHuman(block) {
435
+ process.stdout.write(block + "\n");
436
+ }
437
+ function printResult(json, human, asJson) {
438
+ if (asJson) printJson(json);
439
+ else printHuman(human());
440
+ }
441
+ function warn(message) {
442
+ process.stderr.write(yellow("! " + message) + "\n");
443
+ }
444
+ function presentError(err, opts) {
445
+ const cli = toCliError(err);
446
+ if (opts.json) {
447
+ printJson({ error: cli.message, hint: cli.hint ?? null, exitCode: cli.exitCode });
448
+ } else {
449
+ process.stderr.write(red(bold("\u2717 ") + cli.message) + "\n");
450
+ if (cli.hint) process.stderr.write(dim(" " + cli.hint) + "\n");
451
+ if (opts.verbose && cli.cause) {
452
+ const cause = cli.cause instanceof Error ? cli.cause.stack ?? cli.cause.message : String(cli.cause);
453
+ process.stderr.write(dim("\n" + cause) + "\n");
454
+ } else if (cli.cause && cli.exitCode !== EXIT.GUARD) {
455
+ process.stderr.write(dim(" Run with --verbose for the raw cause.") + "\n");
456
+ }
457
+ }
458
+ return cli.exitCode;
459
+ }
460
+
461
+ // src/action.ts
462
+ function toGlobalOptions(raw) {
463
+ return {
464
+ json: !!raw.json,
465
+ network: raw.network,
466
+ rpc: raw.rpc,
467
+ verbose: !!raw.verbose,
468
+ yes: !!raw.yes,
469
+ acknowledgeEligibility: !!raw.acknowledgeEligibility
470
+ };
471
+ }
472
+ async function runWith(command, fn) {
473
+ const raw = command.optsWithGlobals();
474
+ if (raw.color === false) setColorEnabled(false);
475
+ const ctx = createContext(toGlobalOptions(raw));
476
+ try {
477
+ await fn(ctx);
478
+ } catch (err) {
479
+ process.exitCode = presentError(err, { json: ctx.json, verbose: ctx.verbose });
480
+ }
481
+ }
482
+
483
+ // src/prices.ts
484
+ import { formatUnits, parseUnits } from "viem";
485
+ function usdgAddress(client) {
486
+ return client.network === "testnet" ? TESTNET_ADDRESSES.usdg : MAINNET_ADDRESSES.usdg;
487
+ }
488
+ async function getDexPriceUsd(client, token2, decimals = 18) {
489
+ const usdg = usdgAddress(client);
490
+ if (token2.toLowerCase() === usdg.toLowerCase()) return 1;
491
+ try {
492
+ const quote = await quoteSwap(client, {
493
+ tokenIn: token2,
494
+ tokenOut: usdg,
495
+ amountIn: parseUnits("1", decimals)
496
+ });
497
+ return Number(formatUnits(quote.amountOut, USDG_DECIMALS));
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+ async function getOracleQuote(client, symbol, maxAgeSeconds) {
503
+ try {
504
+ return await getQuote(client, symbol, maxAgeSeconds ? { maxAgeSeconds } : {});
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+ async function getPriceRow(client, symbol, address, options = {}) {
510
+ const [oracle, dexUsd] = await Promise.all([
511
+ getOracleQuote(client, symbol, options.maxAgeSeconds),
512
+ options.withDex === false ? Promise.resolve(null) : getDexPriceUsd(client, address, options.decimals)
513
+ ]);
514
+ const oracleUsd = oracle?.priceUsd ?? null;
515
+ const premium = oracleUsd && dexUsd ? (dexUsd - oracleUsd) / oracleUsd : null;
516
+ return {
517
+ symbol,
518
+ address,
519
+ oracleUsd,
520
+ oracleAgeSeconds: oracle?.ageSeconds ?? null,
521
+ dexUsd,
522
+ premium
523
+ };
524
+ }
525
+
526
+ // src/resolve.ts
527
+ import { getAddress, isAddress } from "viem";
528
+ async function resolveToken(client, input) {
529
+ const raw = input.trim();
530
+ const upper = raw.toUpperCase();
531
+ const testnet = client.network === "testnet";
532
+ if (upper === "USDG") {
533
+ return {
534
+ address: testnet ? TESTNET_ADDRESSES.usdg : MAINNET_ADDRESSES.usdg,
535
+ symbol: "USDG",
536
+ decimals: USDG_DECIMALS,
537
+ isStock: false,
538
+ hasFeed: false
539
+ };
540
+ }
541
+ if (upper === "WETH" || upper === "ETH") {
542
+ return {
543
+ address: testnet ? TESTNET_ADDRESSES.weth : MAINNET_ADDRESSES.weth,
544
+ symbol: "WETH",
545
+ decimals: 18,
546
+ isStock: false,
547
+ hasFeed: false
548
+ };
549
+ }
550
+ if (testnet) {
551
+ const t = TESTNET_STOCK_TOKENS[upper];
552
+ if (t) return { address: t, symbol: upper, decimals: STOCK_TOKEN_DECIMALS, isStock: true, hasFeed: false };
553
+ } else if (isStockTokenSymbol(upper)) {
554
+ const token2 = getStockToken(upper);
555
+ return {
556
+ address: token2.address,
557
+ symbol: token2.symbol,
558
+ decimals: token2.decimals,
559
+ isStock: true,
560
+ hasFeed: token2.feed !== null
561
+ };
562
+ }
563
+ if (isAddress(raw)) {
564
+ const address = getAddress(raw);
565
+ const known = client.network === "mainnet" ? getStockTokenByAddress(address) : null;
566
+ if (known) {
567
+ return {
568
+ address,
569
+ symbol: known.symbol,
570
+ decimals: known.decimals,
571
+ isStock: true,
572
+ hasFeed: known.feed !== null
573
+ };
574
+ }
575
+ const [symbol, decimals] = await Promise.all([
576
+ client.public.readContract({ address, abi: erc20Abi, functionName: "symbol" }).catch(() => "TOKEN"),
577
+ client.public.readContract({ address, abi: erc20Abi, functionName: "decimals" }).catch(() => 18)
578
+ ]);
579
+ return {
580
+ address,
581
+ symbol,
582
+ decimals: Number(decimals),
583
+ isStock: client.network === "mainnet" ? isStockTokenAddress(address) : false,
584
+ hasFeed: false
585
+ };
586
+ }
587
+ if (raw.startsWith("0x")) throw usageError(`"${raw}" is not a valid address.`);
588
+ throw notFoundError(
589
+ `Unknown token "${raw}".`,
590
+ testnet ? `Testnet tokens: USDG, WETH, ${Object.keys(TESTNET_STOCK_TOKENS).join(", ")}, or a 0x address.` : "Use a Stock Token ticker (run `hood stocks`), USDG, WETH, or a 0x address."
591
+ );
592
+ }
593
+
594
+ // src/ui/table.ts
595
+ function renderTable(rows, columns, options = {}) {
596
+ const gap = options.gap ?? 2;
597
+ const maxWidth = options.maxWidth ?? process.stdout.columns ?? 80;
598
+ const measured = columns.map((col) => {
599
+ const cells = rows.map((r) => col.cell(r));
600
+ const contentWidth = Math.max(width(col.header), ...cells.map((c) => width(c)), 0);
601
+ return { col, cells, contentWidth: Math.max(contentWidth, col.min ?? 0) };
602
+ });
603
+ const sep = " ".repeat(gap);
604
+ const totalWidth = (cols) => cols.reduce((sum, m) => sum + m.contentWidth, 0) + gap * Math.max(0, cols.length - 1);
605
+ let visible = measured;
606
+ if (totalWidth(visible) > maxWidth && visible.length > 1) {
607
+ const dropOrder = visible.map((m, i) => ({ i, priority: m.col.priority ?? 100 })).filter((x) => x.i !== 0).sort((a, b) => a.priority - b.priority);
608
+ const dropped = /* @__PURE__ */ new Set();
609
+ for (const { i } of dropOrder) {
610
+ if (totalWidth(visible.filter((_, idx) => !dropped.has(idx))) <= maxWidth) break;
611
+ dropped.add(i);
612
+ }
613
+ visible = measured.filter((_, idx) => !dropped.has(idx));
614
+ }
615
+ const line = (getCell, rowIdx) => visible.map((m) => {
616
+ const raw = getCell(m, rowIdx);
617
+ return m.col.align === "right" ? padStart(raw, m.contentWidth) : padEnd(raw, m.contentWidth);
618
+ }).join(sep);
619
+ const headerLine = visible.map(
620
+ (m) => m.col.align === "right" ? padStart(dim(m.col.header), m.contentWidth) : padEnd(dim(m.col.header), m.contentWidth)
621
+ ).join(sep);
622
+ const rule = gray("\u2500".repeat(Math.min(totalWidth(visible), maxWidth)));
623
+ const body = rows.map((_, rowIdx) => line((m) => m.cells[rowIdx], rowIdx));
624
+ return [headerLine, rule, ...body].join("\n");
625
+ }
626
+ function renderKeyValue(pairs, options = {}) {
627
+ const labelWidth = options.labelWidth ?? Math.max(...pairs.map(([k]) => width(k)), 0);
628
+ return pairs.map(([k, v]) => `${padEnd(dim(k), labelWidth)} ${v}`).join("\n");
629
+ }
630
+
631
+ // src/ui/live.ts
632
+ function liveRegion() {
633
+ const tty = !!process.stdout.isTTY;
634
+ if (!tty) {
635
+ let first = true;
636
+ return {
637
+ render: (frame) => {
638
+ if (!first) process.stdout.write("\n");
639
+ first = false;
640
+ process.stdout.write(frame + "\n");
641
+ },
642
+ stop: () => {
643
+ }
644
+ };
645
+ }
646
+ let prevLines = 0;
647
+ process.stdout.write("\x1B[?25l");
648
+ let stopped = false;
649
+ const stop = () => {
650
+ if (stopped) return;
651
+ stopped = true;
652
+ process.stdout.write("\x1B[?25h");
653
+ };
654
+ process.on("exit", stop);
655
+ const render = (frame) => {
656
+ const lines = frame.split("\n");
657
+ let out = "";
658
+ if (prevLines > 0) out += `\x1B[${prevLines}A`;
659
+ for (const line of lines) out += `\r\x1B[K${line}
660
+ `;
661
+ for (let i = lines.length; i < prevLines; i++) out += "\r\x1B[K\n";
662
+ const extra = Math.max(0, prevLines - lines.length);
663
+ if (extra > 0) out += `\x1B[${extra}A`;
664
+ process.stdout.write(out);
665
+ prevLines = lines.length;
666
+ };
667
+ return { render, stop };
668
+ }
669
+
670
+ // src/ui/spinner.ts
671
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
672
+ function spinner(text) {
673
+ if (!process.stderr.isTTY) {
674
+ process.stderr.write(dim(text) + "\n");
675
+ return {
676
+ update: () => {
677
+ },
678
+ stop: () => {
679
+ },
680
+ succeed: (t) => t && process.stderr.write(dim(t) + "\n"),
681
+ fail: (t) => t && process.stderr.write(dim(t) + "\n")
682
+ };
683
+ }
684
+ let label = text;
685
+ let i = 0;
686
+ const render = () => {
687
+ process.stderr.write(`\r\x1B[K${dim(FRAMES[i % FRAMES.length] + " " + label)}`);
688
+ i += 1;
689
+ };
690
+ render();
691
+ const timer = setInterval(render, 80);
692
+ const clear = () => {
693
+ clearInterval(timer);
694
+ process.stderr.write("\r\x1B[K");
695
+ };
696
+ return {
697
+ update: (t) => {
698
+ label = t;
699
+ },
700
+ stop: clear,
701
+ succeed: (t) => {
702
+ clear();
703
+ if (t) process.stderr.write(dim("\u2713 " + t) + "\n");
704
+ },
705
+ fail: (t) => {
706
+ clear();
707
+ if (t) process.stderr.write(dim("\u2717 " + t) + "\n");
708
+ }
709
+ };
710
+ }
711
+ async function withSpinner(text, fn) {
712
+ const s = spinner(text);
713
+ try {
714
+ const result = await fn(s);
715
+ s.stop();
716
+ return result;
717
+ } catch (err) {
718
+ s.stop();
719
+ throw err;
720
+ }
721
+ }
722
+
723
+ // src/format.ts
724
+ import { formatUnits as formatUnits2 } from "viem";
725
+ function usd(value) {
726
+ if (!Number.isFinite(value)) return "\u2014";
727
+ const abs = Math.abs(value);
728
+ const digits = abs === 0 ? 2 : abs < 0.01 ? 6 : abs < 1 ? 4 : 2;
729
+ return "$" + value.toLocaleString("en-US", {
730
+ minimumFractionDigits: digits,
731
+ maximumFractionDigits: digits
732
+ });
733
+ }
734
+ function num(value, maxFrac = 4) {
735
+ if (!Number.isFinite(value)) return "\u2014";
736
+ return value.toLocaleString("en-US", { maximumFractionDigits: maxFrac });
737
+ }
738
+ function pct(ratio) {
739
+ if (!Number.isFinite(ratio)) return dim("\u2014");
740
+ const p = ratio * 100;
741
+ const sign = p > 0 ? "+" : "";
742
+ const s = `${sign}${p.toFixed(2)}%`;
743
+ if (p > 0) return green(s);
744
+ if (p < 0) return red(s);
745
+ return s;
746
+ }
747
+ function shortAddress(address) {
748
+ if (address.length <= 12) return address;
749
+ return `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
750
+ }
751
+ function addr(a) {
752
+ return shortAddress(a);
753
+ }
754
+ function age(seconds) {
755
+ if (!Number.isFinite(seconds) || seconds < 0) return "\u2014";
756
+ const s = Math.floor(seconds);
757
+ if (s < 60) return `${s}s`;
758
+ const m = Math.floor(s / 60);
759
+ if (m < 60) return `${m}m`;
760
+ const h = Math.floor(m / 60);
761
+ if (h < 24) return `${h}h ${m % 60}m`;
762
+ const d = Math.floor(h / 24);
763
+ return `${d}d ${h % 24}h`;
764
+ }
765
+ function timestamp(unixSeconds) {
766
+ if (!unixSeconds) return "\u2014";
767
+ return new Date(unixSeconds * 1e3).toISOString().replace("T", " ").replace(".000Z", "Z");
768
+ }
769
+ function tokenAmount(raw, decimals, maxFrac = 6) {
770
+ const full = formatUnits2(raw, decimals);
771
+ if (!full.includes(".")) return full;
772
+ const [whole, frac] = full.split(".");
773
+ const trimmed = (frac ?? "").slice(0, maxFrac).replace(/0+$/, "");
774
+ return trimmed ? `${whole}.${trimmed}` : whole;
775
+ }
776
+
777
+ // src/commands/price.ts
778
+ function priceCommand() {
779
+ return new Command("price").description("Chainlink oracle price + DEX price + premium for a Stock Token").argument("<symbol>", "ticker (AAPL) or token address").option("--watch", "live-updating view (repaints in place)", false).option("--interval <ms>", "refresh interval for --watch", "4000").option("--max-age <seconds>", "max acceptable Chainlink answer age").option("--no-dex", "skip the Uniswap price probe (oracle only)").action(
780
+ (symbol, opts, command) => runWith(command, async (ctx) => {
781
+ await price(ctx, symbol, opts);
782
+ })
783
+ );
784
+ }
785
+ async function price(ctx, symbol, opts) {
786
+ const client = ctx.read();
787
+ const token2 = await resolveToken(client, symbol);
788
+ const maxAgeSeconds = opts.maxAge ? Number(opts.maxAge) : void 0;
789
+ const fetchRow = () => getPriceRow(client, token2.symbol, token2.address, {
790
+ maxAgeSeconds,
791
+ withDex: opts.dex,
792
+ decimals: token2.decimals
793
+ });
794
+ if (!opts.watch) {
795
+ const row = await withSpinner(`Pricing ${token2.symbol}\u2026`, () => fetchRow());
796
+ printResult(row, () => renderPrice(row, token2.symbol, ctx.network), ctx.json);
797
+ if (row.oracleUsd === null && row.dexUsd === null) process.exitCode = EXIT.NOT_FOUND;
798
+ return;
799
+ }
800
+ const interval = Math.max(1e3, Number(opts.interval) || 4e3);
801
+ const region = liveRegion();
802
+ const tick = async () => {
803
+ try {
804
+ const row = await fetchRow();
805
+ if (ctx.json) process.stdout.write(JSON.stringify(row, (_k, v) => typeof v === "bigint" ? v.toString() : v) + "\n");
806
+ else region.render(renderPrice(row, token2.symbol, ctx.network, true));
807
+ } catch {
808
+ }
809
+ };
810
+ await tick();
811
+ const timer = setInterval(tick, interval);
812
+ const shutdown = () => {
813
+ clearInterval(timer);
814
+ region.stop();
815
+ process.exit(0);
816
+ };
817
+ process.on("SIGINT", shutdown);
818
+ process.on("SIGTERM", shutdown);
819
+ await new Promise(() => {
820
+ });
821
+ }
822
+ function renderPrice(row, symbol, network, watch2 = false) {
823
+ const pairs = [];
824
+ pairs.push(["Oracle", row.oracleUsd !== null ? bold(usd(row.oracleUsd)) : dim("no fresh feed")]);
825
+ if (row.oracleAgeSeconds !== null) pairs.push(["Updated", dim(age(row.oracleAgeSeconds) + " ago")]);
826
+ pairs.push(["DEX", row.dexUsd !== null ? usd(row.dexUsd) : dim("no pool")]);
827
+ pairs.push(["Premium", row.premium !== null ? pct(row.premium) : dim("\u2014")]);
828
+ pairs.push(["Token", gray(addr(row.address))]);
829
+ const header = `${accent("\u25C8")} ${bold(symbol)} ${dim("\xB7 Robinhood Chain " + network)}`;
830
+ const foot = watch2 ? "\n" + dim(" \u27F3 live \xB7 Ctrl-C to stop") : "";
831
+ return `${header}
832
+ ${renderKeyValue(pairs, { labelWidth: 8 })}${foot}`;
833
+ }
834
+
835
+ // src/commands/stocks.ts
836
+ import { Command as Command2 } from "commander";
837
+
838
+ // src/pmap.ts
839
+ async function pMap(items, mapper, concurrency = 8) {
840
+ const results = new Array(items.length);
841
+ let cursor = 0;
842
+ const limit = Math.max(1, Math.min(concurrency, items.length || 1));
843
+ async function worker() {
844
+ while (cursor < items.length) {
845
+ const index = cursor++;
846
+ results[index] = await mapper(items[index], index);
847
+ }
848
+ }
849
+ await Promise.all(Array.from({ length: limit }, worker));
850
+ return results;
851
+ }
852
+
853
+ // src/commands/stocks.ts
854
+ function stocksCommand() {
855
+ return new Command2("stocks").description("The full Stock Token board with live Chainlink prices").option("--sort <key>", "sort by symbol | price | premium (premium implies --dex)", "symbol").option("--dex", "also probe Uniswap for DEX price + premium (slower)", false).option("--priced", "only tokens with a live Chainlink feed", false).option("--limit <n>", "show at most n rows").action(
856
+ (opts, command) => runWith(command, async (ctx) => {
857
+ await stocks(ctx, opts);
858
+ })
859
+ );
860
+ }
861
+ async function stocks(ctx, opts) {
862
+ const sort = opts.sort;
863
+ if (!["symbol", "price", "premium"].includes(sort)) {
864
+ throw usageError(`--sort must be one of: symbol, price, premium (got "${opts.sort}").`);
865
+ }
866
+ const withDex = opts.dex || sort === "premium";
867
+ const client = ctx.read();
868
+ const tokens = opts.priced || sort === "premium" ? listPricedStockTokens() : listStockTokens();
869
+ const rows = await withSpinner(
870
+ withDex ? `Pricing ${tokens.length} tokens (oracle + DEX)\u2026` : `Pricing ${tokens.length} tokens\u2026`,
871
+ () => pMap(
872
+ tokens,
873
+ async (t) => {
874
+ const oracle = t.feed ? await getOracleQuote(client, t.symbol) : null;
875
+ const dexUsd = withDex ? await getDexPriceUsd(client, t.address, t.decimals) : null;
876
+ const premium = oracle && dexUsd ? (dexUsd - oracle.priceUsd) / oracle.priceUsd : null;
877
+ return {
878
+ symbol: t.symbol,
879
+ name: t.name.replace(/\s*•\s*Robinhood Token$/, ""),
880
+ address: t.address,
881
+ oracleUsd: oracle?.priceUsd ?? null,
882
+ ageSeconds: oracle?.ageSeconds ?? null,
883
+ dexUsd,
884
+ premium
885
+ };
886
+ },
887
+ withDex ? 6 : 16
888
+ )
889
+ );
890
+ sortRows(rows, sort);
891
+ const limited = opts.limit ? rows.slice(0, Math.max(0, Number(opts.limit))) : rows;
892
+ printResult(
893
+ { network: ctx.network, count: limited.length, tokens: limited },
894
+ () => renderBoard(limited, withDex, ctx.network),
895
+ ctx.json
896
+ );
897
+ }
898
+ function sortRows(rows, sort) {
899
+ if (sort === "symbol") rows.sort((a, b) => a.symbol.localeCompare(b.symbol));
900
+ else if (sort === "price") rows.sort((a, b) => (b.oracleUsd ?? -1) - (a.oracleUsd ?? -1));
901
+ else rows.sort((a, b) => (b.premium ?? -Infinity) - (a.premium ?? -Infinity));
902
+ }
903
+ function renderBoard(rows, withDex, network) {
904
+ const columns = [
905
+ { header: "SYMBOL", cell: (r) => bold(r.symbol), priority: 100 },
906
+ { header: "PRICE", align: "right", priority: 90, cell: (r) => r.oracleUsd !== null ? usd(r.oracleUsd) : dim("\u2014") },
907
+ { header: "AGE", align: "right", priority: 40, cell: (r) => r.ageSeconds !== null ? dim(age(r.ageSeconds)) : dim("\u2014") },
908
+ { header: "NAME", priority: 20, cell: (r) => dim(truncate(r.name, 24)) }
909
+ ];
910
+ if (withDex) {
911
+ columns.splice(2, 0, { header: "DEX", align: "right", priority: 70, cell: (r) => r.dexUsd !== null ? usd(r.dexUsd) : dim("\u2014") });
912
+ columns.splice(3, 0, { header: "PREMIUM", align: "right", priority: 80, cell: (r) => r.premium !== null ? pct(r.premium) : dim("\u2014") });
913
+ }
914
+ columns.push({ header: "ADDRESS", priority: 10, cell: (r) => gray(addr(r.address)) });
915
+ const priced = rows.filter((r) => r.oracleUsd !== null).length;
916
+ const header = `${accent("\u25C8")} ${bold("Stock Tokens")} ${dim(`\xB7 ${network} \xB7 ${rows.length} shown \xB7 ${priced} priced`)}`;
917
+ return `${header}
918
+ ${renderTable(rows, columns)}`;
919
+ }
920
+
921
+ // src/commands/coins.ts
922
+ import { Command as Command3 } from "commander";
923
+ import { formatEther } from "viem";
924
+ var ODYSSEY_FACTORIES = [
925
+ ODYSSEY_ADDRESSES.bondingCurveFactory,
926
+ ODYSSEY_ADDRESSES.reflectionFactory,
927
+ ODYSSEY_ADDRESSES.instantFactory
928
+ ];
929
+ function coinsCommand() {
930
+ return new Command3("coins").description("Memecoin screener \u2014 newest or trending launches").option("--new", "newest launches (default)", false).option("--trending", "rank by bonding-curve trade activity", false).option("--lookback <blocks>", "blocks to scan", "50000").option("--limit <n>", "rows to show", "20").option("--names", "resolve token symbols on-chain", false).action(
931
+ (opts, command) => runWith(command, async (ctx) => {
932
+ await coins(ctx, opts);
933
+ })
934
+ );
935
+ }
936
+ async function coins(ctx, opts) {
937
+ if (ctx.network !== "mainnet") throw usageError("The launchpads live on mainnet \u2014 run without --network testnet.");
938
+ const client = ctx.read();
939
+ const limit = Math.max(1, Number(opts.limit) || 20);
940
+ const lookback = BigInt(opts.lookback || "50000");
941
+ const trending = opts.trending;
942
+ const rows = trending ? await withSpinner("Ranking by curve activity\u2026", () => trendingRows(ctx, lookback, limit, opts.names)) : await withSpinner("Fetching newest launches\u2026", () => newRows(ctx, lookback, limit, opts.names));
943
+ printResult(
944
+ { network: ctx.network, mode: trending ? "trending" : "new", count: rows.length, coins: rows },
945
+ () => renderScreener(rows, trending),
946
+ ctx.json
947
+ );
948
+ }
949
+ async function newRows(ctx, lookback, limit, names) {
950
+ const launches2 = await getRecentLaunches(ctx.read(), { lookbackBlocks: lookback });
951
+ const recent = launches2.slice(-limit).reverse();
952
+ return pMap(recent, (l) => launchToRow(ctx, l, names), names ? 8 : 1);
953
+ }
954
+ async function trendingRows(ctx, lookback, limit, names) {
955
+ const client = ctx.read();
956
+ const latest = await client.public.getBlockNumber();
957
+ const fromBlock = latest > lookback ? latest - lookback : 0n;
958
+ const chunk = 10000n;
959
+ const agg = /* @__PURE__ */ new Map();
960
+ for (let start = fromBlock; start <= latest; start += chunk) {
961
+ const end = start + chunk - 1n > latest ? latest : start + chunk - 1n;
962
+ const logs = await client.public.getLogs({
963
+ address: ODYSSEY_FACTORIES,
964
+ event: odysseyTradedEvent,
965
+ fromBlock: start,
966
+ toBlock: end
967
+ });
968
+ for (const log of logs) {
969
+ const token2 = log.args.token.toLowerCase();
970
+ const quote = log.args.quoteAmount ?? 0n;
971
+ const prev = agg.get(token2) ?? { trades: 0, volume: 0n, lastBlock: 0n };
972
+ prev.trades += 1;
973
+ prev.volume += quote;
974
+ if (log.blockNumber > prev.lastBlock) prev.lastBlock = log.blockNumber;
975
+ agg.set(token2, prev);
976
+ }
977
+ }
978
+ const ranked = [...agg.entries()].map(([token2, v]) => ({ token: token2, ...v })).sort((a, b) => b.trades !== a.trades ? b.trades - a.trades : b.volume > a.volume ? 1 : -1).slice(0, limit);
979
+ return pMap(
980
+ ranked,
981
+ async (r) => ({
982
+ token: r.token,
983
+ symbol: names ? await symbolOf(ctx, r.token) : "",
984
+ creator: null,
985
+ pool: null,
986
+ block: r.lastBlock.toString(),
987
+ trades: r.trades,
988
+ volumeEth: Number(formatEther(r.volume))
989
+ }),
990
+ names ? 8 : 1
991
+ );
992
+ }
993
+ async function launchToRow(ctx, l, names) {
994
+ return {
995
+ token: l.token,
996
+ symbol: names ? await symbolOf(ctx, l.token) : "",
997
+ creator: l.creator,
998
+ pool: l.pool,
999
+ block: l.blockNumber.toString(),
1000
+ trades: null,
1001
+ volumeEth: null
1002
+ };
1003
+ }
1004
+ function symbolOf(ctx, token2) {
1005
+ return ctx.read().public.readContract({ address: token2, abi: erc20Abi, functionName: "symbol" }).then((s) => String(s)).catch(() => "");
1006
+ }
1007
+ function renderScreener(rows, trending) {
1008
+ const header = `${accent("\u25C8")} ${bold(trending ? "Trending coins" : "New coins")} ${dim("\xB7 " + rows.length)}`;
1009
+ if (rows.length === 0) {
1010
+ return `${header}
1011
+ ${dim(trending ? "No curve trades in the scanned window." : "No launches in the scanned window \u2014 widen --lookback.")}`;
1012
+ }
1013
+ const columns = [
1014
+ { header: "TOKEN", priority: 100, cell: (r) => r.symbol ? bold(r.symbol) : gray(addr(r.token)) },
1015
+ { header: "ADDRESS", priority: 50, cell: (r) => gray(addr(r.token)) }
1016
+ ];
1017
+ if (trending) {
1018
+ columns.push({ header: "TRADES", align: "right", priority: 95, cell: (r) => green(String(r.trades)) });
1019
+ columns.push({ header: "VOL (ETH)", align: "right", priority: 90, cell: (r) => num(r.volumeEth ?? 0, 4) });
1020
+ } else {
1021
+ columns.push({ header: "CREATOR", priority: 40, cell: (r) => r.creator ? gray(addr(r.creator)) : dim("\u2014") });
1022
+ columns.push({ header: "POOL", priority: 30, cell: (r) => r.pool ? gray(addr(r.pool)) : dim("curve") });
1023
+ }
1024
+ columns.push({ header: "BLOCK", align: "right", priority: 20, cell: (r) => dim("#" + r.block) });
1025
+ return `${header}
1026
+ ${renderTable(rows, columns)}`;
1027
+ }
1028
+
1029
+ // src/commands/launches.ts
1030
+ import { Command as Command4 } from "commander";
1031
+ function launchesCommand() {
1032
+ return new Command4("launches").description("Recent memecoin launches from NOXA and The Odyssey").option("--follow", "stream new launches live", false).option("--launchpad <name>", "noxa | odyssey (default: both)").option("--lookback <blocks>", "blocks to scan for the snapshot", "30000").option("--limit <n>", "max rows in the snapshot", "25").option("--names", "resolve each token symbol on-chain", false).action(
1033
+ (opts, command) => runWith(command, async (ctx) => {
1034
+ await launches(ctx, opts);
1035
+ })
1036
+ );
1037
+ }
1038
+ function validLaunchpad(v) {
1039
+ if (!v) return void 0;
1040
+ if (v !== "noxa" && v !== "odyssey") throw usageError('--launchpad must be "noxa" or "odyssey".');
1041
+ return v;
1042
+ }
1043
+ async function launches(ctx, opts) {
1044
+ const client = ctx.read();
1045
+ const launchpad = validLaunchpad(opts.launchpad);
1046
+ if (opts.follow) {
1047
+ if (ctx.network !== "mainnet") throw usageError("Launch watching runs on mainnet (the launchpads are mainnet-only).");
1048
+ await follow(ctx, launchpad, opts.names);
1049
+ return;
1050
+ }
1051
+ const raw = await withSpinner(
1052
+ "Scanning launchpad logs\u2026",
1053
+ () => getRecentLaunches(client, { lookbackBlocks: BigInt(opts.lookback || "30000"), launchpad })
1054
+ );
1055
+ const recent = raw.slice(-Math.max(1, Number(opts.limit) || 25)).reverse();
1056
+ const rows = await withSpinner(
1057
+ "Building feed\u2026",
1058
+ () => pMap(recent, (l) => toRow(ctx, l, opts.names), opts.names ? 8 : 1)
1059
+ );
1060
+ printResult(
1061
+ { network: ctx.network, count: rows.length, launches: rows },
1062
+ () => renderFeed(rows, ctx.network),
1063
+ ctx.json
1064
+ );
1065
+ }
1066
+ async function toRow(ctx, l, resolveNames) {
1067
+ let symbol = "";
1068
+ if (resolveNames) {
1069
+ symbol = await ctx.read().public.readContract({ address: l.token, abi: erc20Abi, functionName: "symbol" }).then((s) => String(s)).catch(() => "");
1070
+ }
1071
+ return {
1072
+ launchpad: l.launchpad,
1073
+ token: l.token,
1074
+ symbol,
1075
+ creator: l.creator,
1076
+ pool: l.pool,
1077
+ block: l.blockNumber.toString(),
1078
+ tx: l.transactionHash
1079
+ };
1080
+ }
1081
+ async function follow(ctx, launchpad, resolveNames) {
1082
+ const client = ctx.read();
1083
+ process.stderr.write(dim(`Watching ${launchpad ?? "NOXA + Odyssey"} for new launches \u2014 Ctrl-C to stop
1084
+ `));
1085
+ const unwatch = watchLaunches(
1086
+ client,
1087
+ (l) => {
1088
+ void toRow(ctx, l, resolveNames).then((r) => {
1089
+ if (ctx.json) {
1090
+ process.stdout.write(JSON.stringify(r) + "\n");
1091
+ } else {
1092
+ const name = r.symbol ? bold(r.symbol) + " " : "";
1093
+ process.stdout.write(
1094
+ `${green("\u25B2")} ${dim(r.launchpad.padEnd(7))} ${name}${gray(addr(r.token))} ${dim("by")} ${gray(addr(r.creator))} ${dim("#" + r.block)}
1095
+ `
1096
+ );
1097
+ }
1098
+ });
1099
+ },
1100
+ { launchpad, onError: (e) => process.stderr.write(dim(`(watch retry: ${e.message})
1101
+ `)) }
1102
+ );
1103
+ const shutdown = () => {
1104
+ unwatch();
1105
+ process.exit(0);
1106
+ };
1107
+ process.on("SIGINT", shutdown);
1108
+ process.on("SIGTERM", shutdown);
1109
+ await new Promise(() => {
1110
+ });
1111
+ }
1112
+ function renderFeed(rows, network) {
1113
+ const header = `${accent("\u25C8")} ${bold("Launches")} ${dim(`\xB7 ${network} \xB7 ${rows.length}`)}`;
1114
+ if (rows.length === 0) {
1115
+ return `${header}
1116
+ ${dim("No launches in the scanned window. Widen it with --lookback, or --follow to stream new ones.")}`;
1117
+ }
1118
+ const columns = [
1119
+ { header: "BLOCK", align: "right", priority: 90, cell: (r) => dim("#" + r.block) },
1120
+ { header: "PAD", priority: 100, cell: (r) => r.launchpad === "noxa" ? accent("NOXA") : bold("Odyssey") },
1121
+ { header: "TOKEN", priority: 95, cell: (r) => r.symbol ? bold(r.symbol) : gray(addr(r.token)) },
1122
+ { header: "ADDRESS", priority: 50, cell: (r) => gray(addr(r.token)) },
1123
+ { header: "CREATOR", priority: 40, cell: (r) => gray(addr(r.creator)) },
1124
+ { header: "POOL", priority: 30, cell: (r) => r.pool ? gray(addr(r.pool)) : dim("curve") }
1125
+ ];
1126
+ return `${header}
1127
+ ${renderTable(rows, columns)}`;
1128
+ }
1129
+
1130
+ // src/commands/portfolio.ts
1131
+ import { Command as Command5 } from "commander";
1132
+ import { formatEther as formatEther2, getAddress as getAddress2, isAddress as isAddress2 } from "viem";
1133
+ function portfolioCommand() {
1134
+ return new Command5("portfolio").description("Multiplier-correct Stock Token positions + USD totals for an address").argument("<address>", "wallet address to inspect").option("--max-age <seconds>", "max acceptable Chainlink answer age").action(
1135
+ (address, opts, command) => runWith(command, async (ctx) => {
1136
+ await portfolio(ctx, address, opts);
1137
+ })
1138
+ );
1139
+ }
1140
+ async function portfolio(ctx, address, opts) {
1141
+ if (!isAddress2(address)) throw usageError(`"${address}" is not a valid address.`);
1142
+ const owner = getAddress2(address);
1143
+ const client = ctx.read();
1144
+ const maxAgeSeconds = opts.maxAge ? Number(opts.maxAge) : void 0;
1145
+ const { port, usdg, eth } = await withSpinner(`Reading ${addr(owner)}\u2026`, async () => {
1146
+ const [port2, usdg2, eth2] = await Promise.all([
1147
+ getPortfolio(client, owner, maxAgeSeconds ? { maxAgeSeconds } : {}),
1148
+ getUsdgBalance(client, owner),
1149
+ client.public.getBalance({ address: owner })
1150
+ ]);
1151
+ return { port: port2, usdg: usdg2, eth: eth2 };
1152
+ });
1153
+ const held = port.positions.filter((p) => p.balance > 0n);
1154
+ const usdgFloat = Number(formatUsdg(usdg));
1155
+ const grandTotal = port.totalUsd + usdgFloat;
1156
+ const json = {
1157
+ owner,
1158
+ network: ctx.network,
1159
+ eth: formatEther2(eth),
1160
+ usdg: formatUsdg(usdg),
1161
+ stockValueUsd: port.totalUsd,
1162
+ totalUsd: grandTotal,
1163
+ unpricedSymbols: port.unpricedSymbols,
1164
+ positions: held.map((p) => ({
1165
+ symbol: p.symbol,
1166
+ address: p.address,
1167
+ balance: p.balanceTokens,
1168
+ shareEquivalent: p.shareEquivalent,
1169
+ priceUsd: p.quote?.priceUsd ?? null,
1170
+ valueUsd: p.valueUsd
1171
+ }))
1172
+ };
1173
+ printResult(json, () => renderPortfolio(owner, held, eth, usdgFloat, port.totalUsd, grandTotal, port.unpricedSymbols, ctx.network), ctx.json);
1174
+ }
1175
+ function renderPortfolio(owner, held, eth, usdg, stockUsd, total, unpriced, network) {
1176
+ const header = `${accent("\u25C8")} ${bold("Portfolio")} ${dim("\xB7 " + addr(owner) + " \xB7 " + network)}`;
1177
+ const summary = renderKeyValue(
1178
+ [
1179
+ ["ETH", num(Number(formatEther2(eth)), 6)],
1180
+ ["USDG", usd(usdg)],
1181
+ ["Stocks", usd(stockUsd)],
1182
+ ["Total", bold(usd(total))]
1183
+ ],
1184
+ { labelWidth: 7 }
1185
+ );
1186
+ if (held.length === 0) {
1187
+ return `${header}
1188
+ ${summary}
1189
+
1190
+ ${dim("No Stock Token positions. Fund with `hood swap --sell USDG --buy <ticker>`.")}`;
1191
+ }
1192
+ const sorted = [...held].sort((a, b) => (b.valueUsd ?? -1) - (a.valueUsd ?? -1));
1193
+ const columns = [
1194
+ { header: "SYMBOL", priority: 100, cell: (p) => bold(p.symbol) },
1195
+ { header: "BALANCE", align: "right", priority: 90, cell: (p) => num(p.balanceTokens, 4) },
1196
+ { header: "SHARES", align: "right", priority: 60, cell: (p) => num(p.shareEquivalent, 4) },
1197
+ { header: "PRICE", align: "right", priority: 70, cell: (p) => p.quote ? usd(p.quote.priceUsd) : dim("\u2014") },
1198
+ { header: "VALUE", align: "right", priority: 95, cell: (p) => p.valueUsd !== null ? bold(usd(p.valueUsd)) : dim("unpriced") },
1199
+ { header: "ADDRESS", priority: 20, cell: (p) => gray(addr(p.address)) }
1200
+ ];
1201
+ const footNote = unpriced.length > 0 ? "\n" + dim(`Unpriced (no fresh feed): ${unpriced.join(", ")}`) : "";
1202
+ return `${header}
1203
+ ${summary}
1204
+
1205
+ ${renderTable(sorted, columns)}${footNote}`;
1206
+ }
1207
+
1208
+ // src/commands/tx.ts
1209
+ import { Command as Command6 } from "commander";
1210
+ import { decodeEventLog, formatEther as formatEther3, formatGwei, isHash } from "viem";
1211
+
1212
+ // src/blockscout.ts
1213
+ var TESTNET_EXPLORER_URL = "https://explorer.testnet.chain.robinhood.com";
1214
+ function explorerBase(network) {
1215
+ return network === "testnet" ? TESTNET_EXPLORER_URL : MAINNET_EXPLORER_URL;
1216
+ }
1217
+ function txUrl(network, hash) {
1218
+ return `${explorerBase(network)}/tx/${hash}`;
1219
+ }
1220
+ function addressUrl(network, address) {
1221
+ return `${explorerBase(network)}/address/${address}`;
1222
+ }
1223
+ async function get(network, path, timeoutMs = 6e3) {
1224
+ const controller = new AbortController();
1225
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1226
+ try {
1227
+ const res = await fetch(`${explorerBase(network)}/api/v2${path}`, {
1228
+ signal: controller.signal,
1229
+ headers: { accept: "application/json" }
1230
+ });
1231
+ if (!res.ok) return null;
1232
+ return await res.json();
1233
+ } catch {
1234
+ return null;
1235
+ } finally {
1236
+ clearTimeout(timer);
1237
+ }
1238
+ }
1239
+ function fetchTx(network, hash) {
1240
+ return get(network, `/transactions/${hash}`);
1241
+ }
1242
+ function fetchToken(network, address) {
1243
+ return get(network, `/tokens/${address}`);
1244
+ }
1245
+
1246
+ // src/commands/tx.ts
1247
+ function txCommand() {
1248
+ return new Command6("tx").description("Decode a transaction: status, transfers, gas, method").argument("<hash>", "transaction hash").action(
1249
+ (hash, _opts, command) => runWith(command, async (ctx) => {
1250
+ await tx(ctx, hash);
1251
+ })
1252
+ );
1253
+ }
1254
+ async function tx(ctx, hash) {
1255
+ if (!isHash(hash)) throw usageError(`"${hash}" is not a 32-byte transaction hash.`);
1256
+ const client = ctx.read();
1257
+ const { transaction, receipt, block, meta } = await withSpinner("Fetching transaction\u2026", async () => {
1258
+ const transaction2 = await client.public.getTransaction({ hash }).catch(() => null);
1259
+ if (!transaction2) throw notFoundError(`No transaction ${hash} on ${ctx.network}.`);
1260
+ const [receipt2, meta2] = await Promise.all([
1261
+ client.public.getTransactionReceipt({ hash }).catch(() => null),
1262
+ fetchTx(ctx.network, hash)
1263
+ ]);
1264
+ const block2 = receipt2 ? await client.public.getBlock({ blockNumber: receipt2.blockNumber }).catch(() => null) : null;
1265
+ return { transaction: transaction2, receipt: receipt2, block: block2, meta: meta2 };
1266
+ });
1267
+ const raw = [];
1268
+ for (const log of receipt?.logs ?? []) {
1269
+ try {
1270
+ const decoded = decodeEventLog({ abi: erc20Abi, data: log.data, topics: log.topics });
1271
+ if (decoded.eventName !== "Transfer") continue;
1272
+ const args = decoded.args;
1273
+ const known = ctx.network === "mainnet" ? getStockTokenByAddress(log.address) : null;
1274
+ raw.push({
1275
+ token: log.address,
1276
+ from: args.from,
1277
+ to: args.to,
1278
+ rawValue: args.value,
1279
+ knownSymbol: known?.symbol ?? null,
1280
+ knownDecimals: known?.decimals ?? null
1281
+ });
1282
+ } catch {
1283
+ }
1284
+ }
1285
+ const unknownAddresses = [...new Set(raw.filter((r) => r.knownSymbol === null).map((r) => r.token.toLowerCase()))];
1286
+ const resolved = /* @__PURE__ */ new Map();
1287
+ await pMap(
1288
+ unknownAddresses,
1289
+ async (lower) => {
1290
+ const address = raw.find((r) => r.token.toLowerCase() === lower).token;
1291
+ const [symbol, decimals] = await Promise.all([
1292
+ client.public.readContract({ address, abi: erc20Abi, functionName: "symbol" }).catch(() => "TOKEN"),
1293
+ client.public.readContract({ address, abi: erc20Abi, functionName: "decimals" }).catch(() => 18)
1294
+ ]);
1295
+ resolved.set(lower, { symbol: String(symbol), decimals: Number(decimals) });
1296
+ },
1297
+ 8
1298
+ );
1299
+ const transfers = raw.map((r) => {
1300
+ const known = r.knownSymbol !== null && r.knownDecimals !== null;
1301
+ const meta2 = known ? { symbol: r.knownSymbol, decimals: r.knownDecimals } : resolved.get(r.token.toLowerCase());
1302
+ return {
1303
+ token: r.token,
1304
+ symbol: meta2?.symbol ?? null,
1305
+ from: r.from,
1306
+ to: r.to,
1307
+ value: meta2 ? tokenAmount(r.rawValue, meta2.decimals) : r.rawValue.toString()
1308
+ };
1309
+ });
1310
+ const status = receipt ? receipt.status === "success" ? "success" : "reverted" : "pending";
1311
+ const gasUsed = receipt?.gasUsed ?? 0n;
1312
+ const gasPrice = receipt?.effectiveGasPrice ?? transaction.gasPrice ?? 0n;
1313
+ const feeEth = formatEther3(gasUsed * gasPrice);
1314
+ const json = {
1315
+ hash,
1316
+ network: ctx.network,
1317
+ status,
1318
+ block: receipt?.blockNumber?.toString() ?? null,
1319
+ timestamp: block ? Number(block.timestamp) : null,
1320
+ from: transaction.from,
1321
+ to: transaction.to,
1322
+ valueEth: formatEther3(transaction.value),
1323
+ nonce: transaction.nonce,
1324
+ method: meta?.decoded_input?.method_call ?? meta?.method ?? null,
1325
+ gasUsed: gasUsed.toString(),
1326
+ gasPriceGwei: formatGwei(gasPrice),
1327
+ feeEth,
1328
+ transfers,
1329
+ explorer: txUrl(ctx.network, hash)
1330
+ };
1331
+ printResult(json, () => renderTx(json, status, transfers), ctx.json);
1332
+ }
1333
+ function renderTx(j, status, transfers) {
1334
+ const statusBadge = status === "success" ? green("\u25CF success") : status === "reverted" ? red("\u25CF reverted") : dim("\u25CF pending");
1335
+ const header = `${accent("\u25C8")} ${bold("Transaction")} ${dim(addr(j.hash))} ${statusBadge}`;
1336
+ const pairs = [
1337
+ ["From", gray(j.from ? addr(j.from) : "\u2014")],
1338
+ ["To", gray(j.to ? addr(j.to) : dim("contract creation"))],
1339
+ ["Value", j.valueEth === "0" ? dim("0 ETH") : bold(num(Number(j.valueEth), 8) + " ETH")],
1340
+ ["Method", j.method ? bold(j.method) : dim("\u2014")],
1341
+ ["Block", j.block ? "#" + j.block : dim("pending")],
1342
+ ["When", j.timestamp ? dim(timestamp(j.timestamp)) : dim("\u2014")],
1343
+ ["Gas used", dim(num(Number(j.gasUsed), 0) + ` @ ${num(Number(j.gasPriceGwei), 4)} gwei`)],
1344
+ ["Fee", num(Number(j.feeEth), 8) + " ETH"],
1345
+ ["Explorer", gray(j.explorer)]
1346
+ ];
1347
+ let out = `${header}
1348
+ ${renderKeyValue(pairs, { labelWidth: 9 })}`;
1349
+ if (transfers.length) {
1350
+ const lines = transfers.map(
1351
+ (t) => ` ${gray(addr(t.from))} ${dim("\u2192")} ${gray(addr(t.to))} ${bold(t.value)} ${t.symbol ? accent(t.symbol) : gray(addr(t.token))}`
1352
+ );
1353
+ out += `
1354
+
1355
+ ${dim("Token transfers")}
1356
+ ${lines.join("\n")}`;
1357
+ }
1358
+ return out;
1359
+ }
1360
+
1361
+ // src/commands/token.ts
1362
+ import { Command as Command7 } from "commander";
1363
+ function tokenCommand() {
1364
+ return new Command7("token").description("Inspect a token: metadata, supply, multiplier, price").argument("<address>", "token address or ticker").action(
1365
+ (address, _opts, command) => runWith(command, async (ctx) => {
1366
+ await token(ctx, address);
1367
+ })
1368
+ );
1369
+ }
1370
+ async function token(ctx, input) {
1371
+ const client = ctx.read();
1372
+ const resolved = await resolveToken(client, input);
1373
+ const { meta, multiplier, oracle, dexUsd, totalSupply } = await withSpinner(`Reading ${resolved.symbol}\u2026`, async () => {
1374
+ const [meta2, multiplier2, oracle2, dexUsd2, totalSupply2] = await Promise.all([
1375
+ fetchToken(ctx.network, resolved.address),
1376
+ resolved.isStock ? getMultiplier(client, resolved.symbol) : Promise.resolve(null),
1377
+ resolved.hasFeed ? getOracleQuote(client, resolved.symbol) : Promise.resolve(null),
1378
+ getDexPriceUsd(client, resolved.address, resolved.decimals),
1379
+ client.public.readContract({ address: resolved.address, abi: erc20Abi, functionName: "totalSupply" }).catch(() => null)
1380
+ ]);
1381
+ return { meta: meta2, multiplier: multiplier2, oracle: oracle2, dexUsd: dexUsd2, totalSupply: totalSupply2 };
1382
+ });
1383
+ const json = {
1384
+ address: resolved.address,
1385
+ symbol: meta?.symbol ?? resolved.symbol,
1386
+ name: meta?.name ?? null,
1387
+ decimals: resolved.decimals,
1388
+ isStockToken: resolved.isStock,
1389
+ totalSupply: totalSupply !== null ? Number(totalSupply) / 10 ** resolved.decimals : null,
1390
+ holders: meta?.holders_count ? Number(meta.holders_count) : meta?.holders ? Number(meta.holders) : null,
1391
+ uiMultiplier: multiplier !== null ? Number(multiplier) / 1e18 : null,
1392
+ oracleUsd: oracle?.priceUsd ?? null,
1393
+ oracleAgeSeconds: oracle?.ageSeconds ?? null,
1394
+ dexUsd,
1395
+ explorer: addressUrl(ctx.network, resolved.address)
1396
+ };
1397
+ printResult(json, () => renderToken(json, resolved.isStock), ctx.json);
1398
+ }
1399
+ function renderToken(j, isStock) {
1400
+ const header = `${accent("\u25C8")} ${bold(j.symbol)} ${dim(j.name ? "\xB7 " + j.name : "")}`;
1401
+ const pairs = [
1402
+ ["Address", gray(addr(j.address))],
1403
+ ["Decimals", String(j.decimals)],
1404
+ ["Supply", j.totalSupply !== null ? num(j.totalSupply, 0) : dim("\u2014")],
1405
+ ["Holders", j.holders !== null ? num(j.holders, 0) : dim("\u2014")]
1406
+ ];
1407
+ if (isStock) {
1408
+ pairs.push(["Multiplier", j.uiMultiplier !== null ? `${num(j.uiMultiplier, 6)}\xD7` : dim("n/a")]);
1409
+ pairs.push(["Oracle", j.oracleUsd !== null ? bold(usd(j.oracleUsd)) : dim("no feed")]);
1410
+ if (j.oracleAgeSeconds !== null) pairs.push(["Updated", dim(age(j.oracleAgeSeconds) + " ago")]);
1411
+ }
1412
+ pairs.push(["DEX price", j.dexUsd !== null ? usd(j.dexUsd) : dim("no pool")]);
1413
+ pairs.push(["Explorer", gray(j.explorer)]);
1414
+ return `${header}
1415
+ ${renderKeyValue(pairs, { labelWidth: 10 })}`;
1416
+ }
1417
+
1418
+ // src/commands/watch.ts
1419
+ import { Command as Command8 } from "commander";
1420
+ import { formatEther as formatEther4, getAddress as getAddress3, isAddress as isAddress3 } from "viem";
1421
+ function watchCommand() {
1422
+ return new Command8("watch").description("Live activity stream for an address or a token (ERC-20 transfers + native ETH)").argument("<addrOrToken>", "wallet address, token address, or ticker to watch").option("--token", "treat the argument as a token (stream ALL transfers of it)", false).action(
1423
+ (target, opts, command) => runWith(command, async (ctx) => {
1424
+ await watch(ctx, target, opts);
1425
+ })
1426
+ );
1427
+ }
1428
+ async function watch(ctx, target, opts) {
1429
+ const client = ctx.read();
1430
+ if (opts.token || !isAddress3(target)) {
1431
+ const token2 = await resolveToken(client, target);
1432
+ process.stderr.write(dim(`Watching ${token2.symbol} transfers (${addr(token2.address)}) \u2014 Ctrl-C to stop
1433
+ `));
1434
+ const unwatch = watchTransfers(client, { token: token2.address }, (t) => {
1435
+ if (ctx.json) {
1436
+ process.stdout.write(JSON.stringify({ ...t, value: t.value.toString() }) + "\n");
1437
+ } else {
1438
+ process.stdout.write(
1439
+ `${gray(addr(t.from))} ${dim("\u2192")} ${gray(addr(t.to))} ${bold(tokenAmount(t.value, token2.decimals))} ${accent(token2.symbol)} ${dim("#" + t.blockNumber)}
1440
+ `
1441
+ );
1442
+ }
1443
+ });
1444
+ return runUntilInterrupt(unwatch);
1445
+ }
1446
+ if (ctx.network !== "mainnet") throw usageError("Address-mode watch needs the mainnet Stock Token registry \u2014 pass --token for a specific testnet token.");
1447
+ const owner = getAddress3(target);
1448
+ process.stderr.write(dim(`Watching ${addr(owner)} for activity \u2014 Ctrl-C to stop
1449
+ `));
1450
+ const unwatchEth = client.public.watchBlocks({
1451
+ onBlock: async (block) => {
1452
+ for (const txHash of block.transactions) {
1453
+ const tx2 = await client.public.getTransaction({ hash: txHash }).catch(() => null);
1454
+ if (!tx2) continue;
1455
+ if (tx2.from.toLowerCase() !== owner.toLowerCase() && tx2.to?.toLowerCase() !== owner.toLowerCase()) continue;
1456
+ if (tx2.value === 0n) continue;
1457
+ const out = tx2.from.toLowerCase() === owner.toLowerCase();
1458
+ const line = `${out ? red("\u2191") : green("\u2193")} ${bold(formatEther4(tx2.value))} ETH ${dim(out ? "to" : "from")} ${gray(addr(out ? tx2.to : tx2.from))} ${dim("#" + block.number)}`;
1459
+ if (ctx.json) process.stdout.write(JSON.stringify({ kind: "eth", hash: tx2.hash, from: tx2.from, to: tx2.to, valueEth: formatEther4(tx2.value), block: block.number.toString() }) + "\n");
1460
+ else process.stdout.write(line + "\n");
1461
+ }
1462
+ }
1463
+ });
1464
+ const { MAINNET_ADDRESSES: MAINNET_ADDRESSES2 } = await import("./dist-PVZTS5LZ.js");
1465
+ const unwatchUsdg = watchTransfers(client, { token: MAINNET_ADDRESSES2.usdg }, (t) => {
1466
+ if (t.from.toLowerCase() !== owner.toLowerCase() && t.to.toLowerCase() !== owner.toLowerCase()) return;
1467
+ const out = t.from.toLowerCase() === owner.toLowerCase();
1468
+ if (ctx.json) {
1469
+ process.stdout.write(JSON.stringify({ kind: "usdg", ...t, value: t.value.toString() }) + "\n");
1470
+ } else {
1471
+ process.stdout.write(
1472
+ `${out ? red("\u2191") : green("\u2193")} ${bold(tokenAmount(t.value, 6))} USDG ${dim(out ? "to" : "from")} ${gray(addr(out ? t.to : t.from))} ${dim("#" + t.blockNumber)}
1473
+ `
1474
+ );
1475
+ }
1476
+ });
1477
+ return runUntilInterrupt(() => {
1478
+ unwatchEth();
1479
+ unwatchUsdg();
1480
+ });
1481
+ }
1482
+ function runUntilInterrupt(unwatch) {
1483
+ const shutdown = () => {
1484
+ unwatch();
1485
+ process.exit(0);
1486
+ };
1487
+ process.on("SIGINT", shutdown);
1488
+ process.on("SIGTERM", shutdown);
1489
+ return new Promise(() => {
1490
+ });
1491
+ }
1492
+
1493
+ // src/commands/swap.ts
1494
+ import { Command as Command9 } from "commander";
1495
+ import { formatUnits as formatUnits3, parseUnits as parseUnits2 } from "viem";
1496
+
1497
+ // src/spend-cap.ts
1498
+ function checkSpendCap({ maxSpendUsd, estimatedUsd }) {
1499
+ if (maxSpendUsd === void 0) return;
1500
+ if (estimatedUsd === null) return;
1501
+ if (estimatedUsd > maxSpendUsd) {
1502
+ throw guardError(
1503
+ `This spends ~$${estimatedUsd.toFixed(2)}, which exceeds your configured cap of $${maxSpendUsd.toFixed(2)}.`,
1504
+ "Raise the cap with `hood config set maxSpendUsd <n>`, or reduce --amount."
1505
+ );
1506
+ }
1507
+ }
1508
+
1509
+ // src/commands/swap.ts
1510
+ function swapCommand() {
1511
+ return new Command9("swap").description("Quote (default) or execute a Uniswap v3 swap between two tokens").requiredOption("--sell <token>", "ticker or address to sell (e.g. USDG)").requiredOption("--buy <token>", "ticker or address to buy").requiredOption("--amount <amount>", "amount of --sell to spend, in whole tokens").option("--slippage <bps>", "slippage tolerance in basis points", "50").option("--execute", "sign and send (default: quote only)", false).action(
1512
+ (opts, command) => runWith(command, async (ctx) => {
1513
+ await swap(ctx, opts);
1514
+ })
1515
+ );
1516
+ }
1517
+ async function swap(ctx, opts) {
1518
+ const slippageBps = Number(opts.slippage);
1519
+ if (!Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps > 5e3) {
1520
+ throw usageError("--slippage must be a basis-point value between 0 and 5000 (50%).");
1521
+ }
1522
+ const client = opts.execute ? await ctx.wallet() : ctx.read();
1523
+ const [tokenIn, tokenOut] = await Promise.all([resolveToken(client, opts.sell), resolveToken(client, opts.buy)]);
1524
+ const amountIn = parseUnits2(opts.amount, tokenIn.decimals);
1525
+ if (amountIn <= 0n) throw usageError("--amount must be greater than zero.");
1526
+ const quote = await withSpinner(
1527
+ `Quoting ${tokenIn.symbol} \u2192 ${tokenOut.symbol}\u2026`,
1528
+ () => quoteSwap(client, { tokenIn: tokenIn.address, tokenOut: tokenOut.address, amountIn })
1529
+ );
1530
+ const amountOut = formatUnits3(quote.amountOut, tokenOut.decimals);
1531
+ const minOut = formatUnits3(quote.amountOut * BigInt(1e4 - slippageBps) / 10000n, tokenOut.decimals);
1532
+ const rate = Number(amountOut) / Number(opts.amount);
1533
+ const summary = {
1534
+ network: ctx.network,
1535
+ sell: { symbol: tokenIn.symbol, amount: opts.amount },
1536
+ buy: { symbol: tokenOut.symbol, amount: amountOut, minimum: minOut },
1537
+ rate,
1538
+ hops: quote.route.fees.length,
1539
+ slippageBps
1540
+ };
1541
+ if (!opts.execute) {
1542
+ printResult(summary, () => renderQuote(summary, false), ctx.json);
1543
+ return;
1544
+ }
1545
+ const estimatedUsd = await estimateSpendUsd(client, tokenIn.symbol, tokenIn.address, tokenIn.decimals, Number(opts.amount));
1546
+ if (ctx.config.maxSpendUsd !== void 0 && estimatedUsd === null) {
1547
+ warn(`Could not verify this swap against your $${ctx.config.maxSpendUsd} spend cap (no price route for ${tokenIn.symbol}) \u2014 proceeding.`);
1548
+ }
1549
+ checkSpendCap({ maxSpendUsd: ctx.config.maxSpendUsd, estimatedUsd });
1550
+ if (!ctx.json) process.stdout.write(renderQuote(summary, true) + "\n\n");
1551
+ if (!ctx.assumeYes) {
1552
+ const ok = await confirm(`Swap ${opts.amount} ${tokenIn.symbol} for ~${num(Number(amountOut), 6)} ${tokenOut.symbol}?`);
1553
+ if (!ok) throw guardError("Swap cancelled.");
1554
+ }
1555
+ try {
1556
+ const approvalHash = await withSpinner(
1557
+ `Checking ${tokenIn.symbol} allowance\u2026`,
1558
+ () => ensureApproval(client, tokenIn.address, amountIn)
1559
+ );
1560
+ if (approvalHash && !ctx.json) process.stderr.write(dim(`Approved router: ${txUrl(ctx.network, approvalHash)}
1561
+ `));
1562
+ const tx2 = buildSwapTx(client, quote, { slippageBps });
1563
+ const hash = await withSpinner(
1564
+ "Sending swap\u2026",
1565
+ () => client.wallet.sendTransaction({ to: tx2.to, data: tx2.data, value: tx2.value, account: client.account, chain: client.chain })
1566
+ );
1567
+ const receipt = await withSpinner("Confirming\u2026", () => client.public.waitForTransactionReceipt({ hash }));
1568
+ const result = { ...summary, hash, status: receipt.status, explorer: txUrl(ctx.network, hash) };
1569
+ printResult(result, () => renderResult(result), ctx.json);
1570
+ if (receipt.status !== "success") process.exitCode = 1;
1571
+ } catch (err) {
1572
+ if (err instanceof StockTokenEligibilityError) {
1573
+ throw guardError(
1574
+ "Stock Token acquisition is gated for non-affirmed operators.",
1575
+ "Re-run with --acknowledge-eligibility if you are eligible (not a US/CA/UK/CH person)."
1576
+ );
1577
+ }
1578
+ throw err;
1579
+ }
1580
+ }
1581
+ async function estimateSpendUsd(client, symbol, address, decimals, amount) {
1582
+ if (symbol === "USDG") return amount;
1583
+ const perToken = await getDexPriceUsd(client, address, decimals);
1584
+ return perToken !== null ? perToken * amount : null;
1585
+ }
1586
+ function renderQuote(s, confirming) {
1587
+ const header = confirming ? `${yellow("\u26A0")} ${bold("Confirm swap")} ${dim("\xB7 " + s.network)}` : `${accent("\u25C8")} ${bold("Swap quote")} ${dim("\xB7 " + s.network + " \xB7 add --execute to send")}`;
1588
+ const pairs = [
1589
+ ["Sell", bold(s.sell.amount) + " " + s.sell.symbol],
1590
+ ["Buy", bold(`~${s.buy.amount}`) + " " + s.buy.symbol],
1591
+ ["Min. received", dim(s.buy.minimum + " " + s.buy.symbol + ` (${(s.slippageBps / 100).toFixed(2)}% slippage)`)],
1592
+ ["Rate", `1 ${s.sell.symbol} \u2248 ${num(s.rate, 6)} ${s.buy.symbol}`],
1593
+ ["Route", dim(s.hops === 1 ? "direct pool" : `${s.hops}-hop route`)]
1594
+ ];
1595
+ return `${header}
1596
+ ${renderKeyValue(pairs, { labelWidth: 14 })}`;
1597
+ }
1598
+ function renderResult(r) {
1599
+ const badge = r.status === "success" ? green("\u2713 swap confirmed") : dim("\u2717 reverted");
1600
+ return `${badge}
1601
+ ${renderKeyValue(
1602
+ [
1603
+ ["Received", bold(`~${r.buy.amount}`) + " " + r.buy.symbol],
1604
+ ["Tx", gray(addr(r.hash))],
1605
+ ["Explorer", gray(r.explorer)]
1606
+ ],
1607
+ { labelWidth: 9 }
1608
+ )}`;
1609
+ }
1610
+
1611
+ // src/commands/transfer.ts
1612
+ import { Command as Command10 } from "commander";
1613
+ import { formatEther as formatEther5, formatUnits as formatUnits4, getAddress as getAddress4, isAddress as isAddress4, parseEther, parseUnits as parseUnits3 } from "viem";
1614
+ function transferCommand() {
1615
+ return new Command10("transfer").description("Send ETH or an ERC-20 token to an address").requiredOption("--to <address>", "recipient address").requiredOption("--amount <amount>", "amount to send, in whole tokens").option("--token <token>", "ticker or address to send (default: native ETH)").action(
1616
+ (opts, command) => runWith(command, async (ctx) => {
1617
+ await transfer(ctx, opts);
1618
+ })
1619
+ );
1620
+ }
1621
+ async function transfer(ctx, opts) {
1622
+ if (!isAddress4(opts.to)) throw usageError(`"${opts.to}" is not a valid recipient address.`);
1623
+ const to = getAddress4(opts.to);
1624
+ const client = await ctx.wallet();
1625
+ const isNative = !opts.token;
1626
+ const symbol = isNative ? "ETH" : (await resolveToken(client, opts.token)).symbol;
1627
+ const tokenInfo = isNative ? null : await resolveToken(client, opts.token);
1628
+ const amount = isNative ? parseEther(opts.amount) : parseUnits3(opts.amount, tokenInfo.decimals);
1629
+ if (amount <= 0n) throw usageError("--amount must be greater than zero.");
1630
+ const balance = isNative ? await client.public.getBalance({ address: client.account.address }) : await client.public.readContract({ address: tokenInfo.address, abi: erc20Abi, functionName: "balanceOf", args: [client.account.address] });
1631
+ if (balance < amount) {
1632
+ throw guardError(
1633
+ `Insufficient balance: hold ${formatBalance(balance, isNative, tokenInfo?.decimals)} ${symbol}, need ${opts.amount}.`
1634
+ );
1635
+ }
1636
+ const spendToken = isNative ? ctx.network === "testnet" ? TESTNET_ADDRESSES.weth : MAINNET_ADDRESSES.weth : tokenInfo.address;
1637
+ const estimatedUsd = await estimateSpendUsd2(client, symbol, spendToken, isNative ? 18 : tokenInfo.decimals, Number(opts.amount));
1638
+ if (ctx.config.maxSpendUsd !== void 0 && estimatedUsd === null) {
1639
+ warn(`Could not verify this transfer against your $${ctx.config.maxSpendUsd} spend cap (no price route for ${symbol}) \u2014 proceeding.`);
1640
+ }
1641
+ checkSpendCap({ maxSpendUsd: ctx.config.maxSpendUsd, estimatedUsd });
1642
+ if (!ctx.json) {
1643
+ process.stdout.write(
1644
+ renderConfirm({ to, amount: opts.amount, symbol, network: ctx.network }) + "\n\n"
1645
+ );
1646
+ }
1647
+ if (!ctx.assumeYes) {
1648
+ const ok = await confirm(`Send ${opts.amount} ${symbol} to ${addr(to)}?`);
1649
+ if (!ok) throw guardError("Transfer cancelled.");
1650
+ }
1651
+ const hash = await withSpinner(
1652
+ "Sending\u2026",
1653
+ () => isNative ? client.wallet.sendTransaction({ to, value: amount, account: client.account, chain: client.chain }) : client.wallet.writeContract({ address: tokenInfo.address, abi: erc20Abi, functionName: "transfer", args: [to, amount], account: client.account, chain: client.chain })
1654
+ );
1655
+ const receipt = await withSpinner("Confirming\u2026", () => client.public.waitForTransactionReceipt({ hash }));
1656
+ const result = { to, amount: opts.amount, symbol, hash, status: receipt.status, explorer: txUrl(ctx.network, hash) };
1657
+ printResult(result, () => renderResult2(result), ctx.json);
1658
+ if (receipt.status !== "success") process.exitCode = 1;
1659
+ }
1660
+ async function estimateSpendUsd2(client, symbol, address, decimals, amount) {
1661
+ if (symbol === "USDG") return amount;
1662
+ const perToken = await getDexPriceUsd(client, address, decimals);
1663
+ return perToken !== null ? perToken * amount : null;
1664
+ }
1665
+ function formatBalance(balance, isNative, decimals) {
1666
+ return isNative ? formatEther5(balance) : formatUnits4(balance, decimals ?? 18);
1667
+ }
1668
+ function renderConfirm(s) {
1669
+ const header = `${yellow("\u26A0")} ${bold("Confirm transfer")} ${dim("\xB7 " + s.network)}`;
1670
+ const pairs = [
1671
+ ["Amount", bold(s.amount) + " " + s.symbol],
1672
+ ["To", gray(s.to)]
1673
+ ];
1674
+ return `${header}
1675
+ ${renderKeyValue(pairs, { labelWidth: 7 })}`;
1676
+ }
1677
+ function renderResult2(r) {
1678
+ const badge = r.status === "success" ? green("\u2713 transfer confirmed") : dim("\u2717 reverted");
1679
+ return `${badge}
1680
+ ${renderKeyValue(
1681
+ [
1682
+ ["Sent", bold(r.amount) + " " + r.symbol + " \u2192 " + addr(r.to)],
1683
+ ["Tx", gray(addr(r.hash))],
1684
+ ["Explorer", gray(r.explorer)]
1685
+ ],
1686
+ { labelWidth: 9 }
1687
+ )}`;
1688
+ }
1689
+
1690
+ // src/commands/faucet.ts
1691
+ import { Command as Command11 } from "commander";
1692
+ import { formatEther as formatEther6, formatUnits as formatUnits5 } from "viem";
1693
+ var FAUCET_URL = "https://faucet.testnet.chain.robinhood.com/";
1694
+ var CHAINLINK_FAUCET_URL = "https://faucets.chain.link/robinhood-testnet";
1695
+ function faucetCommand() {
1696
+ return new Command11("faucet").description("Print testnet faucet instructions + current testnet balances").action(
1697
+ (_opts, command) => runWith(command, async (ctx) => {
1698
+ await faucet(ctx);
1699
+ })
1700
+ );
1701
+ }
1702
+ async function faucet(ctx) {
1703
+ if (ctx.network !== "testnet") {
1704
+ throw usageError("The faucet is testnet-only.", "Re-run with --network testnet.");
1705
+ }
1706
+ let address = null;
1707
+ let balances = null;
1708
+ try {
1709
+ const client = await ctx.wallet();
1710
+ address = client.account.address;
1711
+ balances = await withSpinner("Reading testnet balances\u2026", async () => {
1712
+ const owner = client.account.address;
1713
+ const [eth, ...tokenBalances] = await Promise.all([
1714
+ client.public.getBalance({ address: owner }),
1715
+ ...Object.entries(TESTNET_STOCK_TOKENS).map(
1716
+ ([symbol, token2]) => client.public.readContract({ address: token2, abi: erc20Abi, functionName: "balanceOf", args: [owner] }).then((b) => ({ symbol, balance: formatUnits5(b, 18) }))
1717
+ )
1718
+ ]);
1719
+ return { eth: formatEther6(eth), tokens: tokenBalances };
1720
+ });
1721
+ } catch {
1722
+ }
1723
+ const json = {
1724
+ network: "testnet",
1725
+ chainId: 46630,
1726
+ faucetUrl: FAUCET_URL,
1727
+ chainlinkFaucetUrl: CHAINLINK_FAUCET_URL,
1728
+ note: "The faucet requires Cloudflare Turnstile + Google Sign-In in a real browser and cannot be automated from a CLI. One claim per 24h drips testnet ETH plus 5 of each: TSLA, AMZN, PLTR, NFLX, AMD.",
1729
+ address,
1730
+ balances
1731
+ };
1732
+ printResult(json, () => renderFaucet(json), ctx.json);
1733
+ }
1734
+ function renderFaucet(j) {
1735
+ const header = `${accent("\u25C8")} ${bold("Testnet faucet")} ${dim("\xB7 chain 46630")}`;
1736
+ const instructions = [
1737
+ dim("The faucet needs a browser session (Cloudflare Turnstile + Google Sign-In) \u2014"),
1738
+ dim("this cannot be automated from a CLI. Claim once per 24h at:"),
1739
+ "",
1740
+ ` ${underline(j.faucetUrl)}`,
1741
+ ` ${dim("Chainlink ETH top-up:")} ${underline(j.chainlinkFaucetUrl)}`,
1742
+ "",
1743
+ dim("Each claim drips testnet ETH + 5 of each: TSLA, AMZN, PLTR, NFLX, AMD.")
1744
+ ].join("\n");
1745
+ if (!j.address || !j.balances) {
1746
+ return `${header}
1747
+ ${instructions}
1748
+
1749
+ ${dim("No wallet configured \u2014 run `hood config set wallet` to see your testnet balances here.")}`;
1750
+ }
1751
+ const rows = [["ETH", num(Number(j.balances.eth), 6)]];
1752
+ for (const t of j.balances.tokens) rows.push([t.symbol, num(Number(t.balance), 4)]);
1753
+ return `${header}
1754
+ ${instructions}
1755
+
1756
+ ${dim("Wallet " + j.address)}
1757
+ ${renderKeyValue(rows, { labelWidth: 6 })}`;
1758
+ }
1759
+
1760
+ // src/commands/deploy-token.ts
1761
+ import { Command as Command12 } from "commander";
1762
+ import { readFileSync as readFileSync3 } from "fs";
1763
+ import { resolve } from "path";
1764
+ import { encodeDeployData } from "viem";
1765
+
1766
+ // src/generated/erc20.ts
1767
+ var HOOD_TOKEN_ABI = [{ "inputs": [{ "internalType": "string", "name": "_name", "type": "string" }, { "internalType": "string", "name": "_symbol", "type": "string" }, { "internalType": "uint8", "name": "_decimals", "type": "uint8" }, { "internalType": "uint256", "name": "_initialSupply", "type": "uint256" }], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [{ "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "Transfer", "type": "event" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }, { "internalType": "address", "name": "", "type": "address" }], "name": "allowance", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "approve", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "", "type": "address" }], "name": "balanceOf", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "symbol", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalSupply", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "transfer", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "value", "type": "uint256" }], "name": "transferFrom", "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "nonpayable", "type": "function" }];
1768
+ var HOOD_TOKEN_BYTECODE = "0x60a060405234801561001057600080fd5b50604051610b29380380610b2983398101604081905261002f91610182565b600061003b85826102a1565b50600161004884826102a1565b5060ff8216608081905260009061006090600a610462565b61006a9083610475565b6002819055336000818152600360205260408082208490555192935090917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef906100b79085815260200190565b60405180910390a3505050505061048c565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126100f057600080fd5b81516001600160401b03811115610109576101096100c9565b604051601f8201601f19908116603f011681016001600160401b0381118282101715610137576101376100c9565b60405281815283820160200185101561014f57600080fd5b60005b8281101561016e57602081860181015183830182015201610152565b506000918101602001919091529392505050565b6000806000806080858703121561019857600080fd5b84516001600160401b038111156101ae57600080fd5b6101ba878288016100df565b602087015190955090506001600160401b038111156101d857600080fd5b6101e4878288016100df565b935050604085015160ff811681146101fb57600080fd5b6060959095015193969295505050565b600181811c9082168061021f57607f821691505b60208210810361023f57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111561029c578282111561029c57806000526020600020601f840160051c6020851015610273575060005b90810190601f840160051c0360005b8181101561029857600083820155600101610282565b5050505b505050565b81516001600160401b038111156102ba576102ba6100c9565b6102ce816102c8845461020b565b84610245565b6020601f82116001811461030257600083156102ea5750848201515b600019600385901b1c1916600184901b17845561035c565b600084815260208120601f198516915b828110156103325787850151825560209485019460019092019101610312565b50848210156103505786840151600019600387901b60f8161c191681555b505060018360011b0184555b5050505050565b634e487b7160e01b600052601160045260246000fd5b6001815b60018411156103b45780850481111561039857610398610363565b60018416156103a657908102905b60019390931c92800261037d565b935093915050565b6000826103cb5750600161045c565b816103d85750600061045c565b81600181146103ee57600281146103f857610414565b600191505061045c565b60ff84111561040957610409610363565b50506001821b61045c565b5060208310610133831016604e8410600b8410161715610437575081810a61045c565b6104446000198484610379565b806000190482111561045857610458610363565b0290505b92915050565b600061046e83836103bc565b9392505050565b808202811582820484141761045c5761045c610363565b6080516106826104a7600039600061010801526106826000f3fe608060405234801561001057600080fd5b50600436106100935760003560e01c8063313ce56711610066578063313ce5671461010357806370a082311461013c57806395d89b411461015c578063a9059cbb14610164578063dd62ed3e1461017757600080fd5b806306fdde0314610098578063095ea7b3146100b657806318160ddd146100d957806323b872dd146100f0575b600080fd5b6100a06101a2565b6040516100ad91906104d2565b60405180910390f35b6100c96100c436600461053c565b610230565b60405190151581526020016100ad565b6100e260025481565b6040519081526020016100ad565b6100c96100fe366004610566565b61029d565b61012a7f000000000000000000000000000000000000000000000000000000000000000081565b60405160ff90911681526020016100ad565b6100e261014a3660046105a3565b60036020526000908152604090205481565b6100a0610362565b6100c961017236600461053c565b61036f565b6100e26101853660046105be565b600460209081526000928352604080842090915290825290205481565b600080546101af906105f1565b80601f01602080910402602001604051908101604052809291908181526020018280546101db906105f1565b80156102285780601f106101fd57610100808354040283529160200191610228565b820191906000526020600020905b81548152906001019060200180831161020b57829003601f168201915b505050505081565b3360008181526004602090815260408083206001600160a01b038716808552925280832085905551919290917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259061028b9086815260200190565b60405180910390a35060015b92915050565b6001600160a01b0383166000908152600460209081526040808320338452909152812054600019811461034e578281101561031f5760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e636500000060448201526064015b60405180910390fd5b610329838261062b565b6001600160a01b03861660009081526004602090815260408083203384529091529020555b610359858585610383565b95945050505050565b600180546101af906105f1565b600061037c338484610383565b9392505050565b60006001600160a01b0383166103e75760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b6064820152608401610316565b6001600160a01b0384166000908152600360205260409020548281101561045f5760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b6064820152608401610316565b6001600160a01b0380861660008181526003602052604080822087860390559287168082529083902080548701905591517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef906104bf9087815260200190565b60405180910390a3506001949350505050565b602081526000825180602084015260005b8181101561050057602081860181015160408684010152016104e3565b506000604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b038116811461053757600080fd5b919050565b6000806040838503121561054f57600080fd5b61055883610520565b946020939093013593505050565b60008060006060848603121561057b57600080fd5b61058484610520565b925061059260208501610520565b929592945050506040919091013590565b6000602082840312156105b557600080fd5b61037c82610520565b600080604083850312156105d157600080fd5b6105da83610520565b91506105e860208401610520565b90509250929050565b600181811c9082168061060557607f821691505b60208210810361062557634e487b7160e01b600052602260045260246000fd5b50919050565b8181038181111561029757634e487b7160e01b600052601160045260246000fdfea2646970667358221220471bc2210cddfb1cf75b9da206267431454db88507c132d3528fab99e5907b0864736f6c63430008240033";
1769
+ var HOOD_TOKEN_COMPILER = { "name": "solc", "version": "0.8.36+commit.8a079791.Emscripten.clang", "optimizer": { "enabled": true, "runs": 200 }, "evmVersion": "paris" };
1770
+
1771
+ // src/commands/deploy-token.ts
1772
+ function deployTokenCommand() {
1773
+ return new Command12("deploy-token").description("Deploy a fixed-supply ERC-20 from a JSON config (direct-rail, no launchpad)").requiredOption("--config <path>", "path to a JSON file: { name, symbol, decimals?, initialSupply }").option("--execute", "sign and send (default: print the plan only)", false).action(
1774
+ (opts, command) => runWith(command, async (ctx) => {
1775
+ await deployToken(ctx, opts);
1776
+ })
1777
+ );
1778
+ }
1779
+ async function deployToken(ctx, opts) {
1780
+ const config = readConfig(opts.config);
1781
+ const decimals = config.decimals ?? 18;
1782
+ const deployData = encodeDeployData({
1783
+ abi: HOOD_TOKEN_ABI,
1784
+ bytecode: HOOD_TOKEN_BYTECODE,
1785
+ args: [config.name, config.symbol, decimals, BigInt(config.initialSupply)]
1786
+ });
1787
+ const plan = {
1788
+ network: ctx.network,
1789
+ name: config.name,
1790
+ symbol: config.symbol,
1791
+ decimals,
1792
+ initialSupply: config.initialSupply,
1793
+ compiler: HOOD_TOKEN_COMPILER.version,
1794
+ bytecodeBytes: (deployData.length - 2) / 2
1795
+ };
1796
+ if (!opts.execute) {
1797
+ printResult(plan, () => renderPlan(plan, false), ctx.json);
1798
+ return;
1799
+ }
1800
+ const client = await ctx.wallet();
1801
+ if (!ctx.json) process.stdout.write(renderPlan(plan, true) + "\n\n");
1802
+ if (!ctx.assumeYes) {
1803
+ const ok = await confirm(`Deploy ${config.symbol} (${num(config.initialSupply, 0)} supply) on ${ctx.network}? This costs gas and cannot be undone.`);
1804
+ if (!ok) throw guardError("Deploy cancelled.");
1805
+ }
1806
+ const hash = await withSpinner(
1807
+ "Deploying\u2026",
1808
+ () => client.wallet.deployContract({
1809
+ abi: HOOD_TOKEN_ABI,
1810
+ bytecode: HOOD_TOKEN_BYTECODE,
1811
+ args: [config.name, config.symbol, decimals, BigInt(config.initialSupply)],
1812
+ account: client.account,
1813
+ chain: client.chain
1814
+ })
1815
+ );
1816
+ const receipt = await withSpinner("Confirming\u2026", () => client.public.waitForTransactionReceipt({ hash }));
1817
+ if (!receipt.contractAddress) {
1818
+ throw guardError("Deployment transaction confirmed but returned no contract address.", "Check the transaction on the explorer.");
1819
+ }
1820
+ const result = {
1821
+ ...plan,
1822
+ hash,
1823
+ status: receipt.status,
1824
+ address: receipt.contractAddress,
1825
+ explorer: txUrl(ctx.network, hash),
1826
+ tokenExplorer: addressUrl(ctx.network, receipt.contractAddress)
1827
+ };
1828
+ printResult(result, () => renderResult3(result), ctx.json);
1829
+ if (receipt.status !== "success") process.exitCode = 1;
1830
+ }
1831
+ function readConfig(path) {
1832
+ let raw;
1833
+ try {
1834
+ raw = readFileSync3(resolve(path), "utf8");
1835
+ } catch {
1836
+ throw usageError(`Could not read config file "${path}".`);
1837
+ }
1838
+ let parsed;
1839
+ try {
1840
+ parsed = JSON.parse(raw);
1841
+ } catch {
1842
+ throw usageError(`"${path}" is not valid JSON.`);
1843
+ }
1844
+ const c = parsed;
1845
+ if (typeof c.name !== "string" || !c.name.trim()) throw usageError('Config "name" must be a non-empty string.');
1846
+ if (typeof c.symbol !== "string" || !c.symbol.trim()) throw usageError('Config "symbol" must be a non-empty string.');
1847
+ if (c.decimals !== void 0 && (!Number.isInteger(c.decimals) || c.decimals < 0 || c.decimals > 255)) {
1848
+ throw usageError('Config "decimals" must be an integer 0-255.');
1849
+ }
1850
+ if (!Number.isInteger(c.initialSupply) || c.initialSupply <= 0) {
1851
+ throw usageError('Config "initialSupply" must be a positive whole-token integer.');
1852
+ }
1853
+ return { name: c.name, symbol: c.symbol, decimals: c.decimals, initialSupply: c.initialSupply };
1854
+ }
1855
+ function renderPlan(p, confirming) {
1856
+ const header = confirming ? `${yellow("\u26A0")} ${bold("Confirm deploy")} ${dim("\xB7 " + p.network)}` : `${accent("\u25C8")} ${bold("Deploy plan")} ${dim("\xB7 " + p.network + " \xB7 add --execute to send")}`;
1857
+ const pairs = [
1858
+ ["Name", bold(p.name)],
1859
+ ["Symbol", bold(p.symbol)],
1860
+ ["Decimals", String(p.decimals)],
1861
+ ["Supply", num(p.initialSupply, 0) + " " + p.symbol],
1862
+ ["Compiler", dim(p.compiler)],
1863
+ ["Bytecode", dim(p.bytecodeBytes + " bytes")]
1864
+ ];
1865
+ return `${header}
1866
+ ${renderKeyValue(pairs, { labelWidth: 9 })}`;
1867
+ }
1868
+ function renderResult3(r) {
1869
+ const badge = r.status === "success" ? green("\u2713 deployed") : dim("\u2717 reverted");
1870
+ return `${badge}
1871
+ ${renderKeyValue(
1872
+ [
1873
+ ["Token", bold(r.symbol) + " " + gray(addr(r.address))],
1874
+ ["Tx", gray(addr(r.hash))],
1875
+ ["Explorer", gray(r.tokenExplorer)]
1876
+ ],
1877
+ { labelWidth: 9 }
1878
+ )}`;
1879
+ }
1880
+
1881
+ // src/commands/config.ts
1882
+ import { Command as Command13 } from "commander";
1883
+ import { getAddress as getAddress5 } from "viem";
1884
+ import { privateKeyToAccount as privateKeyToAccount2, generatePrivateKey } from "viem/accounts";
1885
+ function configCommand() {
1886
+ const cmd = new Command13("config").description("Manage hood-cli settings (rpc, wallet, network)");
1887
+ cmd.command("set <key> [value]").description(`set a config value (${CONFIG_KEYS.join(", ")}, wallet)`).action(async (key, value, _opts, command) => {
1888
+ if (command.optsWithGlobals().color === false) setColorEnabled(false);
1889
+ const json = !!command.optsWithGlobals().json;
1890
+ if (key === "wallet") {
1891
+ await setWallet(json, !!command.optsWithGlobals().yes);
1892
+ return;
1893
+ }
1894
+ if (value === void 0) throw usageError(`"hood config set ${key}" needs a value.`);
1895
+ const config = loadConfig();
1896
+ const next = setConfigValue(config, key, value);
1897
+ saveConfig(next);
1898
+ if (json) printJson({ ok: true, key, value });
1899
+ else printHuman(`${green("\u2713")} ${key} = ${value}`);
1900
+ });
1901
+ cmd.command("get <key>").description("print one config value").action((key, _opts, command) => {
1902
+ const json = !!command.optsWithGlobals().json;
1903
+ const view = redactedConfig(loadConfig());
1904
+ if (!(key in view)) throw usageError(`Unknown config key "${key}".`, `Valid keys: ${Object.keys(view).join(", ")}.`);
1905
+ const value = view[key];
1906
+ if (json) printJson({ [key]: value });
1907
+ else printHuman(String(value ?? dim("unset")));
1908
+ });
1909
+ cmd.command("list").description("print the full config (secrets masked)").action((_opts, command) => {
1910
+ const json = !!command.optsWithGlobals().json;
1911
+ const view = redactedConfig(loadConfig());
1912
+ if (json) {
1913
+ printJson(view);
1914
+ return;
1915
+ }
1916
+ const pairs = Object.entries(view).map(([k, v]) => [k, v === null ? dim("unset") : String(v)]);
1917
+ printHuman(`${accent("\u25C8")} ${bold("hood config")}
1918
+ ${renderKeyValue(pairs, { labelWidth: 14 })}`);
1919
+ });
1920
+ return cmd;
1921
+ }
1922
+ async function setWallet(json, assumeYes) {
1923
+ const config = loadConfig();
1924
+ const keystorePath = config.walletKeystore ?? defaultKeystorePath();
1925
+ if (keystoreExists(keystorePath) && !assumeYes) {
1926
+ const overwrite = await confirm(`A wallet already exists at ${keystorePath}. Overwrite it?`);
1927
+ if (!overwrite) throw guardError("Wallet setup cancelled.");
1928
+ }
1929
+ const importKey = process.env.ROBINHOOD_CHAIN_IMPORT_KEY;
1930
+ const privateKey = importKey ? importKey.startsWith("0x") ? importKey : `0x${importKey}` : generatePrivateKey();
1931
+ const account = privateKeyToAccount2(privateKey);
1932
+ const password = await readPassword("New wallet password (min 8 chars):");
1933
+ const confirmPassword = await readPassword("Confirm password:");
1934
+ if (!passwordsMatch(password, confirmPassword)) throw guardError("Passwords did not match.");
1935
+ writeKeystore(keystorePath, privateKey, getAddress5(account.address), password);
1936
+ const next = { ...config, walletKeystore: keystorePath, walletAddress: account.address };
1937
+ saveConfig(next);
1938
+ const result = { ok: true, address: account.address, keystore: keystorePath, imported: !!importKey };
1939
+ if (json) {
1940
+ printJson(result);
1941
+ return;
1942
+ }
1943
+ printHuman(
1944
+ `${green("\u2713")} Wallet ${result.imported ? "imported" : "generated"}: ${bold(account.address)}
1945
+ ` + dim(` Encrypted keystore: ${keystorePath}`) + (result.imported ? "" : `
1946
+ ${dim(" This is a NEW address \u2014 fund it before sending anything.")}`)
1947
+ );
1948
+ }
1949
+
1950
+ // src/program.ts
1951
+ function readVersion() {
1952
+ const here = dirname3(fileURLToPath(import.meta.url));
1953
+ const pkg = JSON.parse(readFileSync4(join2(here, "..", "package.json"), "utf8"));
1954
+ return pkg.version;
1955
+ }
1956
+ function createProgram() {
1957
+ const program2 = new Command14();
1958
+ program2.name("hood").description("The command-line toolkit for Robinhood Chain (4663) \u2014 instant reads, guarded writes.").version(readVersion(), "-v, --version").option("--json", "machine-readable JSON output").option("--network <net>", "mainnet | testnet", "mainnet").option("--rpc <url>", "override the RPC endpoint").option("--verbose", "show raw error causes").option("--yes", "skip interactive confirmation on writes (still requires --execute)").option("--acknowledge-eligibility", "affirm Stock Token acquisition eligibility (non-US/CA/UK/CH)").option("--no-color", "disable ANSI colour").showHelpAfterError("(run `hood --help` for usage)").configureOutput({
1959
+ outputError: (str, write) => write(str)
1960
+ });
1961
+ program2.addCommand(priceCommand());
1962
+ program2.addCommand(stocksCommand());
1963
+ program2.addCommand(coinsCommand());
1964
+ program2.addCommand(launchesCommand());
1965
+ program2.addCommand(portfolioCommand());
1966
+ program2.addCommand(txCommand());
1967
+ program2.addCommand(tokenCommand());
1968
+ program2.addCommand(watchCommand());
1969
+ program2.addCommand(swapCommand());
1970
+ program2.addCommand(transferCommand());
1971
+ program2.addCommand(faucetCommand());
1972
+ program2.addCommand(deployTokenCommand());
1973
+ program2.addCommand(configCommand());
1974
+ return program2;
1975
+ }
1976
+
1977
+ // src/cli.ts
1978
+ var program = createProgram();
1979
+ program.exitOverride((err) => {
1980
+ if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
1981
+ process.exit(0);
1982
+ }
1983
+ process.exitCode = 2;
1984
+ throw err;
1985
+ });
1986
+ process.on("unhandledRejection", (err) => {
1987
+ process.exitCode = presentError(err, { json: process.argv.includes("--json"), verbose: process.argv.includes("--verbose") });
1988
+ });
1989
+ try {
1990
+ await program.parseAsync(process.argv);
1991
+ } catch (err) {
1992
+ if (process.exitCode === void 0) {
1993
+ process.exitCode = presentError(err, { json: process.argv.includes("--json"), verbose: process.argv.includes("--verbose") });
1994
+ }
1995
+ }
1996
+ //# sourceMappingURL=cli.js.map