@zoralabs/cli 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2936 -0
  2. package/package.json +5 -1
package/dist/index.js ADDED
@@ -0,0 +1,2936 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.tsx
4
+ import { Command as Command9 } from "commander";
5
+ import { ExitPromptError } from "@inquirer/core";
6
+ import "fs";
7
+ import { setApiBaseUrl } from "@zoralabs/coins-sdk";
8
+
9
+ // src/commands/auth.ts
10
+ import { Command } from "commander";
11
+
12
+ // src/lib/config.ts
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ readFileSync,
17
+ writeFileSync,
18
+ chmodSync
19
+ } from "fs";
20
+ import { join } from "path";
21
+ import { homedir, platform } from "os";
22
+ function getConfigDir() {
23
+ if (platform() === "win32") {
24
+ return join(
25
+ process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
26
+ "zora"
27
+ );
28
+ }
29
+ return join(homedir(), ".config", "zora");
30
+ }
31
+ var CONFIG_DIR = getConfigDir();
32
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
33
+ var WALLET_FILE = join(CONFIG_DIR, "wallet.json");
34
+ var CONFIG_VERSION = 1;
35
+ var WALLET_VERSION = 1;
36
+ function assertVersion(parsed, expectedVersion, filePath) {
37
+ if (typeof parsed !== "object" || parsed === null) {
38
+ throw new Error(`${filePath}: expected an object`);
39
+ }
40
+ const obj = parsed;
41
+ if (!("version" in obj)) {
42
+ throw new Error(`${filePath}: missing required field "version"`);
43
+ }
44
+ if (obj.version !== expectedVersion) {
45
+ throw new Error(
46
+ `${filePath}: unsupported version ${obj.version} (expected ${expectedVersion})`
47
+ );
48
+ }
49
+ }
50
+ var configReadOnly = false;
51
+ function readConfig() {
52
+ if (!existsSync(CONFIG_FILE)) return { version: CONFIG_VERSION };
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
56
+ } catch (err) {
57
+ console.error(
58
+ `Warning: could not parse ${CONFIG_FILE}: ${err.message}. Run 'zora auth configure' to fix.`
59
+ );
60
+ configReadOnly = true;
61
+ return { version: CONFIG_VERSION };
62
+ }
63
+ try {
64
+ assertVersion(parsed, CONFIG_VERSION, CONFIG_FILE);
65
+ } catch (err) {
66
+ console.error(
67
+ `Warning: ${err.message}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
68
+ );
69
+ configReadOnly = true;
70
+ return { version: CONFIG_VERSION };
71
+ }
72
+ return parsed;
73
+ }
74
+ var IS_WINDOWS = platform() === "win32";
75
+ function writeSecure(filePath, data) {
76
+ writeFileSync(filePath, data, IS_WINDOWS ? {} : { mode: 384 });
77
+ if (!IS_WINDOWS) {
78
+ chmodSync(filePath, 384);
79
+ }
80
+ }
81
+ function writeConfig(config) {
82
+ mkdirSync(CONFIG_DIR, { recursive: true });
83
+ writeSecure(
84
+ CONFIG_FILE,
85
+ JSON.stringify({ ...config, version: CONFIG_VERSION }, null, 2) + "\n"
86
+ );
87
+ }
88
+ function readWallet() {
89
+ if (!existsSync(WALLET_FILE)) return void 0;
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(readFileSync(WALLET_FILE, "utf-8"));
93
+ } catch (err) {
94
+ throw new Error(`${WALLET_FILE}: ${err.message}`);
95
+ }
96
+ assertVersion(parsed, WALLET_VERSION, WALLET_FILE);
97
+ const obj = parsed;
98
+ if (typeof obj.privateKey !== "string" || !obj.privateKey) {
99
+ throw new Error(`${WALLET_FILE}: missing or invalid "privateKey" field`);
100
+ }
101
+ return parsed;
102
+ }
103
+ function writeWallet(wallet) {
104
+ mkdirSync(CONFIG_DIR, { recursive: true });
105
+ writeSecure(
106
+ WALLET_FILE,
107
+ JSON.stringify({ ...wallet, version: WALLET_VERSION }, null, 2) + "\n"
108
+ );
109
+ }
110
+ function getEnvApiKey() {
111
+ const envKey = process.env.ZORA_API_KEY;
112
+ if (envKey === void 0) return void 0;
113
+ if (!envKey) {
114
+ console.error(
115
+ "ZORA_API_KEY is set but empty. Provide a valid key or unset the variable."
116
+ );
117
+ process.exit(1);
118
+ }
119
+ return envKey;
120
+ }
121
+ function getApiKey() {
122
+ return getEnvApiKey() ?? readConfig().apiKey;
123
+ }
124
+ function saveApiKey(apiKey) {
125
+ const config = readConfig();
126
+ config.apiKey = apiKey;
127
+ writeConfig(config);
128
+ }
129
+ function getPrivateKey() {
130
+ return readWallet()?.privateKey;
131
+ }
132
+ function savePrivateKey(privateKey) {
133
+ writeWallet({ privateKey });
134
+ }
135
+ function getWalletPath() {
136
+ return WALLET_FILE;
137
+ }
138
+ function getAnalyticsId() {
139
+ return readConfig().analyticsId;
140
+ }
141
+ function saveAnalyticsId(id) {
142
+ if (configReadOnly) return;
143
+ const config = readConfig();
144
+ config.analyticsId = id;
145
+ writeConfig(config);
146
+ }
147
+ function getConfigPath() {
148
+ return CONFIG_FILE;
149
+ }
150
+
151
+ // src/lib/mask-key.ts
152
+ function maskKey(key) {
153
+ if (key.length <= 12) return "***";
154
+ return key.slice(0, 8) + "..." + key.slice(-4);
155
+ }
156
+
157
+ // src/lib/output.ts
158
+ var getJson = (cmd) => cmd.optsWithGlobals().json;
159
+ var getYes = (cmd) => cmd.optsWithGlobals().yes ?? false;
160
+ var outputJson = (data) => {
161
+ console.log(JSON.stringify(data, null, 2));
162
+ };
163
+ var outputErrorAndExit = (json, message, suggestion) => {
164
+ if (json) {
165
+ const payload = { error: message };
166
+ if (suggestion) payload.suggestion = suggestion;
167
+ console.log(JSON.stringify(payload, null, 2));
168
+ } else {
169
+ console.error(`\x1B[31mError:\x1B[0m ${message}`);
170
+ if (suggestion) {
171
+ console.error(`\x1B[2m${suggestion}\x1B[0m`);
172
+ }
173
+ }
174
+ process.exit(1);
175
+ };
176
+ var outputData = (json, opts) => {
177
+ if (json) {
178
+ outputJson(opts.json);
179
+ } else {
180
+ opts.table();
181
+ }
182
+ };
183
+
184
+ // src/lib/prompt.ts
185
+ import confirm from "@inquirer/confirm";
186
+ import select from "@inquirer/select";
187
+ import password from "@inquirer/password";
188
+ var confirmOrDefault = async (opts, nonInteractive) => {
189
+ if (nonInteractive) return true;
190
+ return confirm(opts);
191
+ };
192
+ var selectOrDefault = async (opts, nonInteractive) => {
193
+ if (nonInteractive) return opts.default;
194
+ return select(opts);
195
+ };
196
+ var passwordOrFail = async (json, opts, nonInteractive) => {
197
+ if (nonInteractive) {
198
+ outputErrorAndExit(
199
+ json,
200
+ "This command requires interactive input. Remove --yes to proceed."
201
+ );
202
+ }
203
+ return password(opts);
204
+ };
205
+
206
+ // src/lib/analytics.ts
207
+ import { PostHog } from "posthog-node";
208
+ import { createHash, randomUUID } from "crypto";
209
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
210
+
211
+ // src/lib/constants.ts
212
+ var BASE_CHAIN_ID = 8453;
213
+ var WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
214
+ var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
215
+ var ZORA_ADDRESS = "0x1111111111166b7FE7bd91427724B487980aFc69";
216
+ var USDC_DECIMALS = 6;
217
+ var BASE_TRADE_TOKENS = {
218
+ eth: {
219
+ symbol: "ETH",
220
+ decimals: 18,
221
+ trade: { type: "eth" },
222
+ priceAddress: WETH_ADDRESS,
223
+ fixedPriceUsd: void 0
224
+ },
225
+ usdc: {
226
+ symbol: "USDC",
227
+ decimals: USDC_DECIMALS,
228
+ trade: {
229
+ type: "erc20",
230
+ address: USDC_ADDRESS
231
+ },
232
+ priceAddress: USDC_ADDRESS,
233
+ fixedPriceUsd: 1
234
+ },
235
+ zora: {
236
+ symbol: "ZORA",
237
+ decimals: 18,
238
+ trade: {
239
+ type: "erc20",
240
+ address: ZORA_ADDRESS
241
+ },
242
+ priceAddress: ZORA_ADDRESS,
243
+ fixedPriceUsd: void 0
244
+ }
245
+ };
246
+ var POSTHOG_TOKEN = "phc_F3nLidy5mjn4xWQ6PYujO96MVig7UoszINhUUY0usOx";
247
+ var POSTHOG_HOST = "https://us.i.posthog.com";
248
+
249
+ // src/lib/wallet.ts
250
+ import { apiPost } from "@zoralabs/coins-sdk";
251
+ import { createPublicClient, createWalletClient, custom } from "viem";
252
+ import { base } from "viem/chains";
253
+ import { privateKeyToAccount } from "viem/accounts";
254
+ var normalizeKey = (key) => key.startsWith("0x") ? key : `0x${key}`;
255
+ var resolveAccount = (json = false) => {
256
+ const envKey = process.env.ZORA_PRIVATE_KEY;
257
+ const key = envKey || getPrivateKey();
258
+ if (!key) {
259
+ console.error(
260
+ "No wallet configured. Run 'zora setup' to create or import one."
261
+ );
262
+ return process.exit(1);
263
+ }
264
+ try {
265
+ return privateKeyToAccount(normalizeKey(key));
266
+ } catch (err) {
267
+ console.error(
268
+ `\u2717 Invalid private key: ${err instanceof Error ? err.message : String(err)}`
269
+ );
270
+ console.error(" Run 'zora setup --force' to replace it.");
271
+ return process.exit(1);
272
+ }
273
+ };
274
+ function formatRpcError(error) {
275
+ if (typeof error === "string") return error;
276
+ if (error instanceof Error) return error.message;
277
+ if (error && typeof error === "object" && "message" in error) {
278
+ const message = error.message;
279
+ if (typeof message === "string") return message;
280
+ }
281
+ return JSON.stringify(error);
282
+ }
283
+ function createCliRpcTransport(chainId = base.id) {
284
+ return custom({
285
+ async request({
286
+ method,
287
+ params
288
+ }) {
289
+ let response;
290
+ try {
291
+ response = await apiPost("/cli-rpc", {
292
+ chainId,
293
+ method,
294
+ params: params ?? []
295
+ });
296
+ } catch (err) {
297
+ throw new Error(`CLI RPC request failed: ${formatRpcError(err)}`);
298
+ }
299
+ if (response.error) {
300
+ throw new Error(
301
+ `CLI RPC request failed: ${formatRpcError(response.error)}`
302
+ );
303
+ }
304
+ const payload = response.data;
305
+ if (payload && typeof payload === "object" && "error" in payload && payload.error) {
306
+ throw new Error(
307
+ `CLI RPC request failed: ${formatRpcError(payload.error)}`
308
+ );
309
+ }
310
+ if (payload && typeof payload === "object" && "result" in payload) {
311
+ return payload.result;
312
+ }
313
+ return payload;
314
+ }
315
+ });
316
+ }
317
+ function createClients(account) {
318
+ const transport = createCliRpcTransport();
319
+ const publicClient = createPublicClient({
320
+ chain: base,
321
+ transport
322
+ });
323
+ const walletClient = createWalletClient({
324
+ chain: base,
325
+ transport,
326
+ account
327
+ });
328
+ return { publicClient, walletClient };
329
+ }
330
+
331
+ // src/lib/analytics.ts
332
+ var SHUTDOWN_TIMEOUT_MS = 2e3;
333
+ var client = null;
334
+ var distinctId = null;
335
+ var isDisabled = () => process.env.ZORA_NO_ANALYTICS === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI !== void 0 || process.env.NODE_ENV === "test";
336
+ var getOrCreateDistinctId = () => {
337
+ if (distinctId) return distinctId;
338
+ try {
339
+ const stored = getAnalyticsId();
340
+ if (stored) {
341
+ distinctId = stored;
342
+ return distinctId;
343
+ }
344
+ distinctId = randomUUID();
345
+ saveAnalyticsId(distinctId);
346
+ return distinctId;
347
+ } catch {
348
+ distinctId = randomUUID();
349
+ return distinctId;
350
+ }
351
+ };
352
+ var getClient = () => {
353
+ if (!client) {
354
+ client = new PostHog(POSTHOG_TOKEN, { host: POSTHOG_HOST });
355
+ }
356
+ return client;
357
+ };
358
+ var commonProperties = () => ({
359
+ cli_version: true ? "0.2.3" : "development",
360
+ os: process.platform,
361
+ arch: process.arch,
362
+ node_version: process.version
363
+ });
364
+ var hashApiKey = (key) => createHash("sha256").update(key).digest("hex").slice(0, 16);
365
+ var getWalletAddress = () => {
366
+ try {
367
+ const key = process.env.ZORA_PRIVATE_KEY || getPrivateKey();
368
+ if (!key) return void 0;
369
+ return privateKeyToAccount2(normalizeKey(key)).address;
370
+ } catch {
371
+ return void 0;
372
+ }
373
+ };
374
+ var identified = false;
375
+ var identify = () => {
376
+ try {
377
+ if (isDisabled() || identified) return;
378
+ identified = true;
379
+ const id = getOrCreateDistinctId();
380
+ const apiKey = getApiKey();
381
+ const walletAddress = getWalletAddress();
382
+ if (!apiKey && !walletAddress) {
383
+ return;
384
+ }
385
+ getClient().identify({
386
+ distinctId: id,
387
+ properties: {
388
+ api_key_hash: apiKey ? hashApiKey(apiKey) : void 0,
389
+ wallet_address: walletAddress ?? void 0
390
+ }
391
+ });
392
+ } catch {
393
+ }
394
+ };
395
+ var track = (event, properties) => {
396
+ try {
397
+ if (isDisabled()) return;
398
+ getClient().capture({
399
+ distinctId: getOrCreateDistinctId(),
400
+ event,
401
+ properties: { ...commonProperties(), ...properties }
402
+ });
403
+ } catch {
404
+ }
405
+ };
406
+ var shutdownAnalytics = async () => {
407
+ if (!client) return;
408
+ const flushing = client;
409
+ client = null;
410
+ try {
411
+ await Promise.race([
412
+ flushing.shutdown(),
413
+ new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
414
+ ]);
415
+ } catch {
416
+ }
417
+ };
418
+
419
+ // src/commands/auth.ts
420
+ var authCommand = new Command("auth").description(
421
+ "Manage API key authentication.\nAPI key is optional \u2014 without one, requests are rate-limited.\nGet a key at https://zora.co/settings/developer"
422
+ );
423
+ authCommand.command("configure").description("Set your Zora API key").option("--yes", "Skip interactive prompt and execute directly").action(async function() {
424
+ const json = getJson(this);
425
+ const nonInteractive = getYes(this);
426
+ if (getEnvApiKey()) {
427
+ outputData(json, {
428
+ json: {
429
+ status: "env_override",
430
+ message: "API key is set via ZORA_API_KEY environment variable."
431
+ },
432
+ table: () => console.log(
433
+ "API key is set via ZORA_API_KEY environment variable. Unset it to configure manually."
434
+ )
435
+ });
436
+ return;
437
+ }
438
+ const existing = getApiKey();
439
+ if (existing) {
440
+ console.log(`Current key: ${maskKey(existing)}`);
441
+ }
442
+ console.log("Get your API key from: https://zora.co/settings/developer\n");
443
+ const apiKey = await passwordOrFail(
444
+ json,
445
+ { message: "Paste your API key:" },
446
+ nonInteractive
447
+ );
448
+ const trimmed = apiKey.trim();
449
+ if (!trimmed) {
450
+ outputErrorAndExit(
451
+ json,
452
+ "No API key provided.",
453
+ "Usage: zora auth configure"
454
+ );
455
+ }
456
+ try {
457
+ saveApiKey(trimmed);
458
+ outputData(json, {
459
+ json: { saved: true, path: getConfigPath() },
460
+ table: () => console.log(`API key saved to ${getConfigPath()}`)
461
+ });
462
+ track("cli_auth_configure", {
463
+ output_format: json ? "json" : "text"
464
+ });
465
+ } catch (err) {
466
+ outputErrorAndExit(
467
+ json,
468
+ `Failed to save API key: ${err.message}`
469
+ );
470
+ }
471
+ });
472
+ authCommand.command("status").description("Check authentication status").action(function() {
473
+ const json = getJson(this);
474
+ const apiKey = getApiKey();
475
+ if (!apiKey) {
476
+ outputData(json, {
477
+ json: { authenticated: false },
478
+ table: () => {
479
+ console.log(
480
+ "No API key configured. The CLI works without one, but requests are rate-limited."
481
+ );
482
+ console.log(
483
+ "Run 'zora auth configure' to set an API key for higher rate limits."
484
+ );
485
+ }
486
+ });
487
+ track("cli_auth_status", {
488
+ authenticated: false,
489
+ source: null,
490
+ output_format: json ? "json" : "text"
491
+ });
492
+ return;
493
+ }
494
+ const source = getEnvApiKey() ? "env (ZORA_API_KEY)" : getConfigPath();
495
+ outputData(json, {
496
+ json: { authenticated: true, key: maskKey(apiKey), source },
497
+ table: () => {
498
+ console.log(`Authenticated: ${maskKey(apiKey)}`);
499
+ console.log(`Source: ${source}`);
500
+ }
501
+ });
502
+ track("cli_auth_status", {
503
+ authenticated: true,
504
+ source: getEnvApiKey() ? "env" : "file",
505
+ output_format: json ? "json" : "text"
506
+ });
507
+ });
508
+
509
+ // src/commands/balance.ts
510
+ import { Command as Command3 } from "commander";
511
+ import { getProfileBalances, setApiKey as setApiKey2 } from "@zoralabs/coins-sdk";
512
+
513
+ // src/lib/render.tsx
514
+ import { renderToString } from "ink";
515
+ var renderOnce = (element) => {
516
+ const output = renderToString(element);
517
+ console.log(output);
518
+ };
519
+
520
+ // src/components/table.tsx
521
+ import { Box, Text } from "ink";
522
+ import { jsx, jsxs } from "react/jsx-runtime";
523
+ var truncate = (str, max) => {
524
+ if (str.length <= max) return str;
525
+ return str.slice(0, max - 1) + "\u2026";
526
+ };
527
+ var TableComponent = ({
528
+ columns,
529
+ data,
530
+ title,
531
+ subtitle
532
+ }) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [
533
+ title && /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, marginBottom: 1, children: [
534
+ /* @__PURE__ */ jsx(Text, { bold: true, children: title }),
535
+ subtitle && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
536
+ " ",
537
+ subtitle
538
+ ] })
539
+ ] }),
540
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 1, children: columns.map((col) => /* @__PURE__ */ jsx(Box, { width: col.width, children: /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: col.header }) }, col.header)) }),
541
+ data.map((row, i) => /* @__PURE__ */ jsx(Box, { paddingLeft: 1, children: columns.map((col) => {
542
+ const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), col.width - 2);
543
+ const colorName = col.color?.(row);
544
+ return /* @__PURE__ */ jsx(Box, { width: col.width, children: /* @__PURE__ */ jsx(Text, { color: colorName, children: value }) }, col.header);
545
+ }) }, i))
546
+ ] });
547
+
548
+ // src/commands/explore.tsx
549
+ import { Command as Command2 } from "commander";
550
+ import {
551
+ setApiKey,
552
+ getCoinsTopVolume24h,
553
+ getCoinsMostValuable,
554
+ getCoinsNew,
555
+ getCoinsTopGainers,
556
+ getCoinsLastTraded,
557
+ getCoinsLastTradedUnique,
558
+ getExploreTopVolumeAll24h,
559
+ getExploreTopVolumeCreators24h,
560
+ getExploreNewAll,
561
+ getExploreFeaturedCreators,
562
+ getExploreFeaturedVideos,
563
+ getCreatorCoins,
564
+ getMostValuableCreatorCoins,
565
+ getMostValuableAll,
566
+ getMostValuableTrends,
567
+ getNewTrends,
568
+ getTopVolumeTrends24h,
569
+ getTrendingAll,
570
+ getTrendingCreators,
571
+ getTrendingPosts,
572
+ getTrendingTrends
573
+ } from "@zoralabs/coins-sdk";
574
+
575
+ // src/lib/format.ts
576
+ import { format, formatDistanceStrict } from "date-fns";
577
+ import { formatEther } from "viem";
578
+ var ANSI_CODES = {
579
+ dim: ["\x1B[2m", "\x1B[22m"],
580
+ bold: ["\x1B[1m", "\x1B[22m"]
581
+ };
582
+ function styledText(text, style) {
583
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
584
+ if (!useColor) return text;
585
+ const [open, close] = ANSI_CODES[style];
586
+ return `${open}${text}${close}`;
587
+ }
588
+ function formatCurrency(value) {
589
+ if (!value || Number(value) === 0) return "$0";
590
+ return new Intl.NumberFormat("en-US", {
591
+ style: "currency",
592
+ currency: "USD",
593
+ notation: "compact",
594
+ minimumFractionDigits: 1,
595
+ maximumFractionDigits: 1
596
+ }).format(Number(value));
597
+ }
598
+ var NO_CHANGE = { text: "-", color: void 0 };
599
+ function formatMcapChange(marketCap, delta) {
600
+ if (!delta || !marketCap) return NO_CHANGE;
601
+ const currentMCap = Number(marketCap);
602
+ const absoluteDelta = Number(delta);
603
+ const previousMCap = currentMCap - absoluteDelta;
604
+ if (currentMCap === 0 || previousMCap === 0) return NO_CHANGE;
605
+ const percentChange = absoluteDelta / previousMCap * 100;
606
+ const plusPrefix = percentChange >= 0 ? "+" : "";
607
+ const text = `${plusPrefix}${percentChange.toFixed(1)}%`;
608
+ const color = percentChange > 0 ? "green" : percentChange < 0 ? "red" : void 0;
609
+ return { text, color };
610
+ }
611
+ function formatUsd(value) {
612
+ return new Intl.NumberFormat("en-US", {
613
+ style: "currency",
614
+ currency: "USD",
615
+ minimumFractionDigits: 2,
616
+ maximumFractionDigits: 2
617
+ }).format(value);
618
+ }
619
+ function formatHolders(count) {
620
+ return new Intl.NumberFormat("en-US").format(count);
621
+ }
622
+ function formatRelativeTime(date, now = /* @__PURE__ */ new Date()) {
623
+ const diffMs = now.getTime() - date.getTime();
624
+ if (diffMs < 6e4) return "just now";
625
+ return formatDistanceStrict(date, now, { addSuffix: true });
626
+ }
627
+ function formatAbsoluteTime(date) {
628
+ return format(date, "yyyy-MM-dd h:mm a");
629
+ }
630
+ function formatCreatedAt(isoDate, now) {
631
+ if (!isoDate) return "-";
632
+ const date = new Date(isoDate);
633
+ if (isNaN(date.getTime())) return "-";
634
+ return `${formatRelativeTime(date, now)} (${formatAbsoluteTime(date)})`;
635
+ }
636
+ var formatEthDisplay = (wei) => {
637
+ const eth = formatEther(wei);
638
+ const parts = eth.split(".");
639
+ if (!parts[1]) return eth;
640
+ const trimmed = parts[1].replace(/0+$/, "") || "0";
641
+ return `${parts[0]}.${trimmed}`;
642
+ };
643
+ var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
644
+ maximumFractionDigits: 2
645
+ }).format(Number(coinsOut));
646
+
647
+ // src/lib/types.ts
648
+ var SORT_LABELS = {
649
+ mcap: "Top by Market Cap",
650
+ volume: "Top by 24h Volume",
651
+ new: "New",
652
+ gainers: "Top Gainers (24h)",
653
+ "last-traded": "Last Traded",
654
+ "last-traded-unique": "Last Traded (Unique)",
655
+ trending: "Trending",
656
+ featured: "Featured"
657
+ };
658
+ var TYPE_LABELS = {
659
+ all: "all",
660
+ trend: "trends",
661
+ "creator-coin": "creator coins",
662
+ post: "posts"
663
+ };
664
+ var COIN_TYPE_DISPLAY = {
665
+ CONTENT: "post",
666
+ CREATOR: "creator-coin",
667
+ TREND: "trend"
668
+ };
669
+
670
+ // src/commands/explore.tsx
671
+ import { Box as Box2, Text as Text2 } from "ink";
672
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
673
+ var QUERY_MAP = {
674
+ mcap: {
675
+ all: getMostValuableAll,
676
+ trend: getMostValuableTrends,
677
+ "creator-coin": getMostValuableCreatorCoins,
678
+ post: getCoinsMostValuable
679
+ },
680
+ volume: {
681
+ all: getExploreTopVolumeAll24h,
682
+ trend: getTopVolumeTrends24h,
683
+ "creator-coin": getExploreTopVolumeCreators24h,
684
+ post: getCoinsTopVolume24h
685
+ },
686
+ new: {
687
+ all: getExploreNewAll,
688
+ trend: getNewTrends,
689
+ "creator-coin": getCreatorCoins,
690
+ post: getCoinsNew
691
+ },
692
+ gainers: {
693
+ post: getCoinsTopGainers
694
+ },
695
+ "last-traded": {
696
+ post: getCoinsLastTraded
697
+ },
698
+ "last-traded-unique": {
699
+ post: getCoinsLastTradedUnique
700
+ },
701
+ trending: {
702
+ all: getTrendingAll,
703
+ trend: getTrendingTrends,
704
+ "creator-coin": getTrendingCreators,
705
+ post: getTrendingPosts
706
+ },
707
+ featured: {
708
+ "creator-coin": getExploreFeaturedCreators,
709
+ post: getExploreFeaturedVideos
710
+ }
711
+ };
712
+ var formatCompactCurrency = (value) => {
713
+ if (!value) return "$0";
714
+ return new Intl.NumberFormat("en-US", {
715
+ style: "currency",
716
+ currency: "USD",
717
+ notation: "compact",
718
+ maximumFractionDigits: 1
719
+ }).format(Number(value));
720
+ };
721
+ var formatChange = (marketCap, delta) => {
722
+ if (!delta || !marketCap) return "-";
723
+ const cap = Number(marketCap);
724
+ const d = Number(delta);
725
+ if (cap === 0) return "-";
726
+ const prevCap = cap - d;
727
+ if (prevCap === 0) return "-";
728
+ const pct = d / prevCap * 100;
729
+ const sign = pct >= 0 ? "+" : "";
730
+ return `${sign}${pct.toFixed(1)}%`;
731
+ };
732
+ var changeColor = (row) => {
733
+ if (!row.marketCapDelta24h || !row.marketCap) return void 0;
734
+ const cap = Number(row.marketCap);
735
+ const d = Number(row.marketCapDelta24h);
736
+ if (cap === 0 || cap - d === 0) return void 0;
737
+ const pct = d / (cap - d) * 100;
738
+ if (pct > 0) return "green";
739
+ if (pct < 0) return "red";
740
+ return void 0;
741
+ };
742
+ var SORT_OPTIONS = Object.keys(SORT_LABELS).join(", ");
743
+ var rankColumn = {
744
+ header: "#",
745
+ width: 5,
746
+ accessor: (r) => String(r.rank)
747
+ };
748
+ var exploreColumns = [
749
+ { header: "Name", width: 27, accessor: (r) => r.name ?? "Unknown" },
750
+ { header: "Address", width: 44, accessor: (r) => r.address ?? "" },
751
+ {
752
+ header: "Type",
753
+ width: 16,
754
+ accessor: (r) => COIN_TYPE_DISPLAY[r.coinType ?? ""] ?? r.coinType ?? ""
755
+ },
756
+ {
757
+ header: "Market Cap",
758
+ width: 14,
759
+ accessor: (r) => formatCompactCurrency(r.marketCap)
760
+ },
761
+ {
762
+ header: "24h Vol",
763
+ width: 14,
764
+ accessor: (r) => formatCompactCurrency(r.volume24h)
765
+ },
766
+ {
767
+ header: "24h Change",
768
+ width: 12,
769
+ accessor: (r) => formatChange(r.marketCap, r.marketCapDelta24h),
770
+ color: changeColor
771
+ }
772
+ ];
773
+ var exploreCommand = new Command2("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS}`, "mcap").option(
774
+ "--type <type>",
775
+ "Filter by type: all, trend, creator-coin, post (availability varies by sort)",
776
+ "post"
777
+ ).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
778
+ const json = getJson(this);
779
+ const sort = opts.sort;
780
+ const type = opts.type;
781
+ const limit = parseInt(opts.limit, 10);
782
+ const after = opts.after;
783
+ if (isNaN(limit) || limit <= 0 || limit > 20) {
784
+ outputErrorAndExit(
785
+ json,
786
+ `Invalid --limit value: ${opts.limit}. Must be an integer between 1 and 20.`,
787
+ "Usage: zora explore --limit 10"
788
+ );
789
+ }
790
+ if (!QUERY_MAP[sort]) {
791
+ outputErrorAndExit(
792
+ json,
793
+ `Invalid --sort value: ${sort}.`,
794
+ `Supported: ${SORT_OPTIONS}`
795
+ );
796
+ }
797
+ if (!QUERY_MAP[sort][type]) {
798
+ const supported = Object.keys(QUERY_MAP[sort]);
799
+ outputErrorAndExit(
800
+ json,
801
+ `Invalid --type for --sort ${sort}.`,
802
+ `Supported: ${supported.join(", ")}`
803
+ );
804
+ }
805
+ const apiKey = getApiKey();
806
+ if (apiKey) {
807
+ setApiKey(apiKey);
808
+ }
809
+ const queryFn = QUERY_MAP[sort][type];
810
+ let response;
811
+ try {
812
+ response = await queryFn({ count: limit, after });
813
+ } catch (err) {
814
+ outputErrorAndExit(
815
+ json,
816
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
817
+ );
818
+ }
819
+ if (response.error) {
820
+ const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
821
+ outputErrorAndExit(json, `API error: ${msg}`);
822
+ }
823
+ const edges = response.data?.exploreList?.edges ?? [];
824
+ const coins = edges.map((e) => e.node);
825
+ const pageInfo = response.data?.exploreList?.pageInfo;
826
+ if (coins.length === 0) {
827
+ outputData(json, {
828
+ json: { coins: [], pageInfo: pageInfo ?? null },
829
+ table: () => {
830
+ renderOnce(
831
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, marginTop: 1, children: [
832
+ /* @__PURE__ */ jsx2(Text2, { children: "No coins found." }),
833
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
834
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Try a different sort or type (defaults to posts):" }),
835
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " zora explore --sort volume --type all" }),
836
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " zora explore --sort new --type all" })
837
+ ] })
838
+ ] })
839
+ );
840
+ }
841
+ });
842
+ return;
843
+ }
844
+ const rankedCoins = coins.map((c, i) => ({ ...c, rank: i + 1 }));
845
+ const columns = after ? exploreColumns : [rankColumn, ...exploreColumns];
846
+ const title = type !== "all" ? `${SORT_LABELS[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS[sort];
847
+ const subtitle = `${coins.length} result${coins.length !== 1 ? "s" : ""}`;
848
+ outputData(json, {
849
+ json: { coins, pageInfo: pageInfo ?? null },
850
+ table: () => {
851
+ renderOnce(
852
+ /* @__PURE__ */ jsx2(
853
+ TableComponent,
854
+ {
855
+ columns,
856
+ data: rankedCoins,
857
+ title,
858
+ subtitle
859
+ }
860
+ )
861
+ );
862
+ if (pageInfo?.hasNextPage && pageInfo.endCursor) {
863
+ console.log(
864
+ `
865
+ ${styledText(`Next page: zora explore --sort ${sort} --type ${type} --limit ${limit} --after ${pageInfo.endCursor}`, "dim")}`
866
+ );
867
+ }
868
+ }
869
+ });
870
+ track("cli_explore", {
871
+ sort,
872
+ type,
873
+ limit,
874
+ paginated: after !== void 0,
875
+ result_count: coins.length,
876
+ has_next_page: pageInfo?.hasNextPage ?? false,
877
+ output_format: json ? "json" : "text"
878
+ });
879
+ });
880
+
881
+ // src/lib/balance-format.ts
882
+ var COIN_DECIMALS = 18;
883
+ var toHumanBalance = (rawBalance) => Number(normalizeTokenAmount(rawBalance));
884
+ var normalizeTokenAmount = (rawBalance, decimals = COIN_DECIMALS) => {
885
+ try {
886
+ const value = BigInt(rawBalance);
887
+ const divisor = 10n ** BigInt(decimals);
888
+ const whole = value / divisor;
889
+ const fraction = value % divisor;
890
+ if (fraction === 0n) return whole.toString();
891
+ const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
892
+ return `${whole}.${fractionText}`;
893
+ } catch {
894
+ console.warn(`Warning: could not parse token amount "${rawBalance}"`);
895
+ return rawBalance;
896
+ }
897
+ };
898
+ var formatUsdValue = (balance, priceInUsdc) => {
899
+ if (!priceInUsdc) return "-";
900
+ const value = toHumanBalance(balance) * Number(priceInUsdc);
901
+ if (value < 0.01) return "<$0.01";
902
+ return formatUsd(value);
903
+ };
904
+ var formatBalance = (balance) => {
905
+ const n = toHumanBalance(balance);
906
+ if (n === 0) return "0";
907
+ if (n < 1e-3) return "<0.001";
908
+ if (n < 1) return n.toFixed(4);
909
+ return new Intl.NumberFormat("en-US", {
910
+ notation: "compact",
911
+ compactDisplay: "long",
912
+ maximumFractionDigits: 1
913
+ }).format(n);
914
+ };
915
+ var trimTrailingZeros = (value) => {
916
+ if (!value.includes(".")) return value;
917
+ const trimmed = value.replace(/0+$/, "").replace(/\.$/, "");
918
+ return trimmed || "0";
919
+ };
920
+
921
+ // src/lib/wallet-balances.ts
922
+ import { getTokenInfo } from "@zoralabs/coins-sdk";
923
+ import {
924
+ createPublicClient as createPublicClient2,
925
+ erc20Abi,
926
+ formatUnits,
927
+ http
928
+ } from "viem";
929
+ import { base as base2 } from "viem/chains";
930
+ var TRACKED_TOKENS = [
931
+ {
932
+ name: "Ether",
933
+ symbol: "ETH",
934
+ address: WETH_ADDRESS,
935
+ decimals: 18,
936
+ priceAddress: WETH_ADDRESS,
937
+ isNative: true
938
+ },
939
+ {
940
+ name: "USD Coin",
941
+ symbol: "USDC",
942
+ address: USDC_ADDRESS,
943
+ decimals: USDC_DECIMALS,
944
+ priceAddress: USDC_ADDRESS,
945
+ fixedPriceUsd: 1
946
+ },
947
+ {
948
+ name: "ZORA",
949
+ symbol: "ZORA",
950
+ address: ZORA_ADDRESS,
951
+ decimals: 18,
952
+ priceAddress: ZORA_ADDRESS
953
+ }
954
+ ];
955
+ var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
956
+ try {
957
+ const res = await getTokenInfo({ address, chainId });
958
+ return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
959
+ } catch (err) {
960
+ console.warn(
961
+ `Warning: failed to fetch price for ${address}: ${err instanceof Error ? err.message : String(err)}`
962
+ );
963
+ return null;
964
+ }
965
+ };
966
+ var fetchWalletBalances = async (walletAddress) => {
967
+ const publicClient = createPublicClient2({ chain: base2, transport: http() });
968
+ const nativeToken = TRACKED_TOKENS.find((t) => t.isNative);
969
+ const erc20Tokens = TRACKED_TOKENS.filter((t) => !t.isNative);
970
+ const [ethBalance, multicallResults] = await Promise.all([
971
+ publicClient.getBalance({ address: walletAddress }),
972
+ publicClient.multicall({
973
+ contracts: erc20Tokens.map((t) => ({
974
+ address: t.address,
975
+ abi: erc20Abi,
976
+ functionName: "balanceOf",
977
+ args: [walletAddress]
978
+ }))
979
+ })
980
+ ]);
981
+ const rawBalances = /* @__PURE__ */ new Map();
982
+ if (nativeToken) rawBalances.set(nativeToken, ethBalance);
983
+ erc20Tokens.forEach((token, i) => {
984
+ if (multicallResults[i].status === "success") {
985
+ rawBalances.set(token, multicallResults[i].result);
986
+ } else {
987
+ console.warn(`Warning: failed to fetch balance for ${token.symbol}`);
988
+ rawBalances.set(token, 0n);
989
+ }
990
+ });
991
+ const priceResults = await Promise.allSettled(
992
+ TRACKED_TOKENS.map(async (token) => {
993
+ const balance = rawBalances.get(token) ?? 0n;
994
+ let priceUsd = null;
995
+ if (token.fixedPriceUsd != null) {
996
+ priceUsd = token.fixedPriceUsd;
997
+ } else if (balance > 0n || token.isNative) {
998
+ priceUsd = await fetchTokenPriceUsd(token.priceAddress);
999
+ }
1000
+ return { token, balance, priceUsd };
1001
+ })
1002
+ );
1003
+ const resolved = priceResults.map((result, i) => {
1004
+ if (result.status === "fulfilled") return result.value;
1005
+ const token = TRACKED_TOKENS[i];
1006
+ console.warn(`Warning: failed to resolve token ${token.symbol}`);
1007
+ return { token, balance: rawBalances.get(token) ?? 0n, priceUsd: null };
1008
+ });
1009
+ const visible = resolved.filter((r) => r.balance > 0n || r.token.isNative);
1010
+ const intermediate = visible.map(({ token, balance, priceUsd }) => {
1011
+ const human = formatUnits(balance, token.decimals);
1012
+ const usdValue = priceUsd !== null ? Number(human) * priceUsd : null;
1013
+ return { token, human, priceUsd, usdValue };
1014
+ });
1015
+ const walletBalances = intermediate.map(
1016
+ ({ token, human, usdValue }) => ({
1017
+ name: token.name,
1018
+ symbol: token.symbol,
1019
+ balance: trimTrailingZeros(human),
1020
+ usdValue: usdValue !== null ? formatUsd(usdValue) : "-"
1021
+ })
1022
+ );
1023
+ const walletBalancesJson = intermediate.map(
1024
+ ({ token, human, priceUsd, usdValue }) => ({
1025
+ name: token.name,
1026
+ symbol: token.symbol,
1027
+ address: token.isNative ? null : token.address,
1028
+ balance: trimTrailingZeros(human),
1029
+ priceUsd,
1030
+ usdValue: usdValue !== null ? Number(usdValue.toFixed(6)) : null
1031
+ })
1032
+ );
1033
+ return { walletBalances, walletBalancesJson };
1034
+ };
1035
+
1036
+ // src/commands/balance.ts
1037
+ var SORT_MAP = {
1038
+ "usd-value": "USD_VALUE",
1039
+ balance: "BALANCE",
1040
+ "market-cap": "MARKET_CAP",
1041
+ "price-change": "PRICE_CHANGE"
1042
+ };
1043
+ var SORT_LABELS2 = {
1044
+ "usd-value": "USD Value",
1045
+ balance: "Balance",
1046
+ "market-cap": "Market Cap",
1047
+ "price-change": "Price Change"
1048
+ };
1049
+ var SORT_OPTIONS2 = Object.keys(SORT_MAP).join(", ");
1050
+ var extractErrorMessage = (error) => {
1051
+ if (typeof error === "object" && error !== null && "error" in error) {
1052
+ return String(error.error);
1053
+ }
1054
+ return JSON.stringify(error);
1055
+ };
1056
+ var changeColor2 = (row) => {
1057
+ if (!row.coin?.marketCap || !row.coin.marketCapDelta24h) return void 0;
1058
+ const cap = Number(row.coin.marketCap);
1059
+ const d = Number(row.coin.marketCapDelta24h);
1060
+ if (cap === 0 || cap - d === 0) return void 0;
1061
+ const pct = d / (cap - d) * 100;
1062
+ if (pct > 0) return "green";
1063
+ if (pct < 0) return "red";
1064
+ return void 0;
1065
+ };
1066
+ var walletColumns = [
1067
+ { header: "Name", width: 14, accessor: (row) => row.name },
1068
+ {
1069
+ header: "Symbol",
1070
+ width: 10,
1071
+ noTruncate: true,
1072
+ accessor: (row) => row.symbol
1073
+ },
1074
+ { header: "Balance", width: 20, accessor: (row) => row.balance },
1075
+ { header: "USD Value", width: 16, accessor: (row) => row.usdValue }
1076
+ ];
1077
+ var balanceColumns = [
1078
+ { header: "#", width: 5, accessor: (row) => String(row.rank) },
1079
+ { header: "Name", width: 24, accessor: (row) => row.coin?.name ?? "Unknown" },
1080
+ {
1081
+ header: "Symbol",
1082
+ width: 12,
1083
+ noTruncate: true,
1084
+ accessor: (row) => row.coin?.symbol ?? ""
1085
+ },
1086
+ {
1087
+ header: "Balance",
1088
+ width: 14,
1089
+ accessor: (row) => formatBalance(row.balance)
1090
+ },
1091
+ {
1092
+ header: "USD Value",
1093
+ width: 14,
1094
+ accessor: (row) => formatUsdValue(row.balance, row.coin?.tokenPrice?.priceInUsdc)
1095
+ },
1096
+ {
1097
+ header: "Market Cap",
1098
+ width: 14,
1099
+ accessor: (row) => formatCompactCurrency(row.coin?.marketCap)
1100
+ },
1101
+ {
1102
+ header: "24h Change",
1103
+ width: 12,
1104
+ accessor: (row) => formatChange(row.coin?.marketCap, row.coin?.marketCapDelta24h),
1105
+ color: changeColor2
1106
+ }
1107
+ ];
1108
+ var formatBalanceJson = (balance, rank) => {
1109
+ const priceUsd = balance.coin?.tokenPrice?.priceInUsdc;
1110
+ const marketCap = balance.coin?.marketCap ? Number(balance.coin.marketCap) : null;
1111
+ const marketCapDelta24h = balance.coin?.marketCapDelta24h ? Number(balance.coin.marketCapDelta24h) : null;
1112
+ const volume24h = balance.coin?.volume24h ? Number(balance.coin.volume24h) : null;
1113
+ const totalVolume = balance.coin?.totalVolume ? Number(balance.coin.totalVolume) : null;
1114
+ const priceUsdValue = priceUsd ? Number(priceUsd) : null;
1115
+ const usdValue = priceUsdValue !== null ? Number((toHumanBalance(balance.balance) * priceUsdValue).toFixed(6)) : null;
1116
+ const marketCapChange24h = marketCap !== null && marketCapDelta24h !== null && marketCap - marketCapDelta24h !== 0 ? Number(
1117
+ (marketCapDelta24h / (marketCap - marketCapDelta24h) * 100).toFixed(
1118
+ 4
1119
+ )
1120
+ ) : null;
1121
+ return {
1122
+ rank,
1123
+ name: balance.coin?.name ?? null,
1124
+ symbol: balance.coin?.symbol ?? null,
1125
+ coinType: balance.coin?.coinType ?? null,
1126
+ chainId: balance.coin?.chainId ?? null,
1127
+ address: balance.coin?.address ?? null,
1128
+ creatorHandle: balance.coin?.creatorProfile?.handle ?? null,
1129
+ previewImage: balance.coin?.mediaContent?.previewImage?.medium ?? null,
1130
+ balance: normalizeTokenAmount(balance.balance),
1131
+ usdValue,
1132
+ priceUsd: priceUsdValue,
1133
+ marketCap,
1134
+ marketCapDelta24h,
1135
+ marketCapChange24h,
1136
+ volume24h,
1137
+ totalVolume
1138
+ };
1139
+ };
1140
+ function resolveContext(json) {
1141
+ const account = resolveAccount(json);
1142
+ const apiKey = getApiKey();
1143
+ if (!apiKey) {
1144
+ outputErrorAndExit(
1145
+ json,
1146
+ "Not authenticated. Run 'zora auth configure' to set your API key."
1147
+ );
1148
+ }
1149
+ setApiKey2(apiKey);
1150
+ return account;
1151
+ }
1152
+ function renderWallet(json, walletResult) {
1153
+ outputData(json, {
1154
+ json: { wallet: walletResult.walletBalancesJson },
1155
+ table: () => {
1156
+ renderOnce(
1157
+ TableComponent({
1158
+ columns: walletColumns,
1159
+ data: walletResult.walletBalances,
1160
+ title: "Wallet"
1161
+ })
1162
+ );
1163
+ }
1164
+ });
1165
+ }
1166
+ function renderCoins(json, balances, total, sort) {
1167
+ const rankedBalances = balances.map((balance, index) => ({
1168
+ ...balance,
1169
+ rank: index + 1
1170
+ }));
1171
+ outputData(json, {
1172
+ json: {
1173
+ coins: rankedBalances.map(
1174
+ (balance) => formatBalanceJson(balance, balance.rank)
1175
+ )
1176
+ },
1177
+ table: () => {
1178
+ if (balances.length === 0) {
1179
+ console.log("\n No coin balances found.\n");
1180
+ console.log(" Buy coins to see them here:");
1181
+ console.log(" zora buy <address> --eth 0.001\n");
1182
+ } else {
1183
+ renderOnce(
1184
+ TableComponent({
1185
+ columns: balanceColumns,
1186
+ data: rankedBalances,
1187
+ title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
1188
+ subtitle: `${balances.length} of ${total}`
1189
+ })
1190
+ );
1191
+ }
1192
+ }
1193
+ });
1194
+ }
1195
+ async function fetchCoins(json, address, sort, limit) {
1196
+ let response;
1197
+ try {
1198
+ response = await getProfileBalances({
1199
+ identifier: address,
1200
+ count: limit,
1201
+ sortOption: SORT_MAP[sort]
1202
+ });
1203
+ } catch (err) {
1204
+ outputErrorAndExit(
1205
+ json,
1206
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1207
+ );
1208
+ }
1209
+ if (response.error) {
1210
+ outputErrorAndExit(
1211
+ json,
1212
+ `API error: ${extractErrorMessage(response.error)}`
1213
+ );
1214
+ }
1215
+ const edges = response.data?.profile?.coinBalances?.edges ?? [];
1216
+ const balances = edges.map(
1217
+ (e) => e.node
1218
+ );
1219
+ const total = response.data?.profile?.coinBalances?.count ?? balances.length;
1220
+ return { balances, total };
1221
+ }
1222
+ function validateCoinOpts(json, sort, limitStr) {
1223
+ if (!SORT_MAP[sort]) {
1224
+ outputErrorAndExit(
1225
+ json,
1226
+ `Invalid --sort value: ${sort}.`,
1227
+ `Supported: ${SORT_OPTIONS2}`
1228
+ );
1229
+ }
1230
+ const limit = parseInt(limitStr, 10);
1231
+ if (isNaN(limit) || limit <= 0 || limit > 20) {
1232
+ outputErrorAndExit(
1233
+ json,
1234
+ `Invalid --limit value: ${limitStr}. Must be an integer between 1 and 20.`
1235
+ );
1236
+ }
1237
+ return { sort, limit };
1238
+ }
1239
+ var balanceCommand = new Command3("balance").description("Show balances in your wallet").action(async function() {
1240
+ const json = getJson(this);
1241
+ const account = resolveContext(json);
1242
+ const sort = "usd-value";
1243
+ const limit = 10;
1244
+ let walletResult;
1245
+ let coinsResult;
1246
+ try {
1247
+ [walletResult, coinsResult] = await Promise.all([
1248
+ fetchWalletBalances(account.address),
1249
+ fetchCoins(json, account.address, sort, limit)
1250
+ ]);
1251
+ } catch (err) {
1252
+ outputErrorAndExit(
1253
+ json,
1254
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1255
+ );
1256
+ }
1257
+ const rankedBalances = coinsResult.balances.map((balance, index) => ({
1258
+ ...balance,
1259
+ rank: index + 1
1260
+ }));
1261
+ outputData(json, {
1262
+ json: {
1263
+ wallet: walletResult.walletBalancesJson,
1264
+ coins: rankedBalances.map(
1265
+ (balance) => formatBalanceJson(balance, balance.rank)
1266
+ )
1267
+ },
1268
+ table: () => {
1269
+ renderOnce(
1270
+ TableComponent({
1271
+ columns: walletColumns,
1272
+ data: walletResult.walletBalances,
1273
+ title: "Wallet"
1274
+ })
1275
+ );
1276
+ if (coinsResult.balances.length === 0) {
1277
+ console.log("\n No coin balances found.\n");
1278
+ console.log(" Buy coins to see them here:");
1279
+ console.log(" zora buy <address> --eth 0.001\n");
1280
+ } else {
1281
+ renderOnce(
1282
+ TableComponent({
1283
+ columns: balanceColumns,
1284
+ data: rankedBalances,
1285
+ title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
1286
+ subtitle: `${coinsResult.balances.length} of ${coinsResult.total}`
1287
+ })
1288
+ );
1289
+ }
1290
+ }
1291
+ });
1292
+ track("cli_balances", {
1293
+ sort,
1294
+ limit,
1295
+ result_count: coinsResult.balances.length,
1296
+ total_count: coinsResult.total,
1297
+ output_format: json ? "json" : "text"
1298
+ });
1299
+ });
1300
+ balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").action(async function() {
1301
+ const json = getJson(this);
1302
+ const account = resolveContext(json);
1303
+ let walletResult;
1304
+ try {
1305
+ walletResult = await fetchWalletBalances(account.address);
1306
+ } catch (err) {
1307
+ outputErrorAndExit(
1308
+ json,
1309
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
1310
+ );
1311
+ }
1312
+ renderWallet(json, walletResult);
1313
+ });
1314
+ balanceCommand.command("coins").description("Show coin positions").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "usd-value").option("--limit <n>", "Number of results (max 20)", "10").action(async function(opts) {
1315
+ const json = getJson(this);
1316
+ const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
1317
+ const account = resolveContext(json);
1318
+ const { balances, total } = await fetchCoins(
1319
+ json,
1320
+ account.address,
1321
+ sort,
1322
+ limit
1323
+ );
1324
+ renderCoins(json, balances, total, sort);
1325
+ });
1326
+
1327
+ // src/commands/buy.ts
1328
+ import { Command as Command4 } from "commander";
1329
+ import confirm2 from "@inquirer/confirm";
1330
+ import { parseUnits, formatUnits as formatUnits3, isAddress } from "viem";
1331
+ import {
1332
+ setApiKey as setApiKey3,
1333
+ getCoin,
1334
+ tradeCoin,
1335
+ createTradeCall
1336
+ } from "@zoralabs/coins-sdk";
1337
+
1338
+ // src/lib/trade-helpers.ts
1339
+ import {
1340
+ parseEther,
1341
+ formatUnits as formatUnits2,
1342
+ isAddressEqual,
1343
+ parseEventLogs,
1344
+ erc20Abi as erc20Abi2
1345
+ } from "viem";
1346
+ var GAS_RESERVE = parseEther("0.001");
1347
+ var BUY_AMOUNT_CHECKS = {
1348
+ eth: (opts) => opts.eth !== void 0,
1349
+ usd: (opts) => opts.usd !== void 0,
1350
+ percent: (opts) => opts.percent !== void 0,
1351
+ all: (opts) => opts.all === true
1352
+ };
1353
+ var SELL_AMOUNT_CHECKS = {
1354
+ amount: (opts) => opts.amount !== void 0,
1355
+ usd: (opts) => opts.usd !== void 0,
1356
+ percent: (opts) => opts.percent !== void 0,
1357
+ all: (opts) => opts.all === true
1358
+ };
1359
+ var getAmountMode = (json, opts, checks, flagNames) => {
1360
+ const provided = Object.entries(checks).filter(([, isProvided]) => isProvided(opts)).map(([mode]) => mode);
1361
+ if (provided.length === 0) {
1362
+ outputErrorAndExit(json, `Specify one amount flag: ${flagNames}`);
1363
+ }
1364
+ if (provided.length > 1) {
1365
+ outputErrorAndExit(json, `Only one amount flag allowed: ${flagNames}`);
1366
+ }
1367
+ return provided[0];
1368
+ };
1369
+ var parsePercentageLikeValue = (value) => {
1370
+ if (!/^\d+(\.\d+)?$/.test(value)) return void 0;
1371
+ const parsed = Number(value);
1372
+ return Number.isFinite(parsed) ? parsed : void 0;
1373
+ };
1374
+ var formatAmountDisplay = (amount, decimals) => {
1375
+ const formatted = formatUnits2(amount, decimals);
1376
+ const parts = formatted.split(".");
1377
+ if (!parts[1]) {
1378
+ return new Intl.NumberFormat("en-US", {
1379
+ maximumFractionDigits: 2
1380
+ }).format(Number(formatted));
1381
+ }
1382
+ const twoDecimal = `${parts[0]}.${parts[1].slice(0, 2)}`;
1383
+ let maxDecimals = 2;
1384
+ if (Number(twoDecimal) === 0 && amount > 0n) {
1385
+ const sigIndex = parts[1].search(/[1-9]/);
1386
+ maxDecimals = sigIndex === -1 ? 6 : Math.min(sigIndex + 4, parts[1].length);
1387
+ }
1388
+ const truncated = `${parts[0]}.${parts[1].slice(0, maxDecimals)}`;
1389
+ return new Intl.NumberFormat("en-US", {
1390
+ maximumFractionDigits: maxDecimals
1391
+ }).format(Number(truncated));
1392
+ };
1393
+ var getReceivedAmountFromReceipt = ({
1394
+ receipt,
1395
+ tokenAddress,
1396
+ recipient
1397
+ }) => {
1398
+ const transfers = parseEventLogs({
1399
+ abi: erc20Abi2,
1400
+ eventName: "Transfer",
1401
+ logs: receipt.logs,
1402
+ strict: false
1403
+ });
1404
+ const matchingTransfers = transfers.filter((transfer) => {
1405
+ const to = transfer.args?.to;
1406
+ if (!to) return false;
1407
+ return isAddressEqual(transfer.address, tokenAddress) && isAddressEqual(to, recipient);
1408
+ });
1409
+ if (matchingTransfers.length === 0) {
1410
+ throw new Error("No matching Transfer event found in receipt.");
1411
+ }
1412
+ return matchingTransfers.reduce((total, transfer) => {
1413
+ const value = transfer.args?.value;
1414
+ if (value === void 0) {
1415
+ throw new Error("Transfer event missing amount.");
1416
+ }
1417
+ return total + value;
1418
+ }, 0n);
1419
+ };
1420
+ var printDebugRequest = (label, tradeParameters) => {
1421
+ if (process.env.ZORA_API_TARGET) {
1422
+ console.error(`[debug] API target: ${process.env.ZORA_API_TARGET}`);
1423
+ }
1424
+ console.error(`
1425
+ [debug] ${label} \u2014 Quote Request:`);
1426
+ console.error(
1427
+ JSON.stringify(
1428
+ {
1429
+ tokenIn: tradeParameters.sell,
1430
+ tokenOut: tradeParameters.buy,
1431
+ amountIn: tradeParameters.amountIn.toString(),
1432
+ slippage: tradeParameters.slippage,
1433
+ chainId: 8453,
1434
+ sender: tradeParameters.sender,
1435
+ recipient: tradeParameters.recipient || tradeParameters.sender
1436
+ },
1437
+ null,
1438
+ 2
1439
+ )
1440
+ );
1441
+ };
1442
+ var printDebugResponse = (label, quoteResponse) => {
1443
+ console.error(`
1444
+ [debug] ${label} \u2014 Quote Response:`);
1445
+ console.error(JSON.stringify(quoteResponse, null, 2));
1446
+ console.error("");
1447
+ };
1448
+ var printQuote = (json, info) => {
1449
+ if (json) {
1450
+ outputJson({
1451
+ action: "quote",
1452
+ coin: info.coinSymbol,
1453
+ address: info.address,
1454
+ spend: {
1455
+ amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
1456
+ raw: info.amountIn.toString(),
1457
+ symbol: info.inputTokenSymbol
1458
+ },
1459
+ estimated: {
1460
+ amount: formatUnits2(BigInt(info.amountOut), 18),
1461
+ raw: info.amountOut,
1462
+ symbol: info.coinSymbol
1463
+ },
1464
+ slippage: info.slippagePct
1465
+ });
1466
+ return;
1467
+ }
1468
+ console.log(`
1469
+ Buy ${info.coinName} (${info.coinSymbol})
1470
+ `);
1471
+ console.log(` Amount ${info.spendAmount}`);
1472
+ console.log(` You get ~${info.coinsFormatted} ${info.coinSymbol}`);
1473
+ console.log(` Slippage ${info.slippagePct}%
1474
+ `);
1475
+ };
1476
+ var printTradeResult = (json, info) => {
1477
+ const receivedAmount = formatUnits2(info.receivedAmountOut, 18);
1478
+ const receivedFormatted = formatCoinsDisplay(receivedAmount);
1479
+ if (json) {
1480
+ outputJson({
1481
+ action: "buy",
1482
+ coin: info.coinSymbol,
1483
+ address: info.address,
1484
+ spent: {
1485
+ amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
1486
+ raw: info.amountIn.toString(),
1487
+ symbol: info.inputTokenSymbol
1488
+ },
1489
+ received: {
1490
+ amount: receivedAmount,
1491
+ raw: info.receivedAmountOut.toString(),
1492
+ symbol: info.coinSymbol
1493
+ },
1494
+ tx: info.txHash
1495
+ });
1496
+ return;
1497
+ }
1498
+ console.log(`
1499
+ Bought ${info.coinName}
1500
+ `);
1501
+ console.log(` Spent ${info.spendAmount} ${info.inputTokenSymbol}`);
1502
+ console.log(` Received ${receivedFormatted} ${info.coinSymbol}`);
1503
+ console.log(` Tx ${info.txHash}
1504
+ `);
1505
+ };
1506
+
1507
+ // src/commands/buy.ts
1508
+ var buyCommand = new Command4("buy").description("Buy a coin").argument("<address>", "Coin contract address (0x\u2026)").option("--eth <value>", "Buy with ETH amount").option("--usd <value>", "Buy with USD equivalent (use with --token)").option("--token <asset>", "Token to spend: eth, usdc, zora", "eth").option("--percent <value>", "Buy with percentage of ETH balance").option("--all", "Swap all ETH for coin").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").option("-o, --output <format>", "Output format: table, json", "table").action(async (coinAddress, opts) => {
1509
+ const json = opts.output === "json";
1510
+ const debug = opts.debug === true;
1511
+ if (!isAddress(coinAddress)) {
1512
+ outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
1513
+ }
1514
+ const output = opts.output;
1515
+ if (output !== "table" && output !== "json") {
1516
+ outputErrorAndExit(
1517
+ false,
1518
+ `Invalid --output value: ${output}. Use: table, json`
1519
+ );
1520
+ }
1521
+ const tokenKey = opts.token.toLowerCase();
1522
+ if (!(tokenKey in BASE_TRADE_TOKENS)) {
1523
+ outputErrorAndExit(
1524
+ json,
1525
+ `Invalid --token value: ${opts.token}. Use: eth, usdc, zora`
1526
+ );
1527
+ }
1528
+ const inputToken = BASE_TRADE_TOKENS[tokenKey];
1529
+ const amountMode = getAmountMode(
1530
+ json,
1531
+ opts,
1532
+ BUY_AMOUNT_CHECKS,
1533
+ "--eth, --usd, --percent, or --all"
1534
+ );
1535
+ const slippagePct = parsePercentageLikeValue(opts.slippage);
1536
+ if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
1537
+ outputErrorAndExit(
1538
+ json,
1539
+ "Invalid --slippage value. Must be between 0 and 99."
1540
+ );
1541
+ }
1542
+ const slippage = slippagePct / 100;
1543
+ const apiKey = getApiKey();
1544
+ if (apiKey) {
1545
+ setApiKey3(apiKey);
1546
+ }
1547
+ const account = resolveAccount(json);
1548
+ const { publicClient, walletClient } = createClients(account);
1549
+ let token;
1550
+ try {
1551
+ const response = await getCoin({ address: coinAddress });
1552
+ token = response.data?.zora20Token;
1553
+ } catch (err) {
1554
+ outputErrorAndExit(
1555
+ json,
1556
+ `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
1557
+ );
1558
+ }
1559
+ if (!token) {
1560
+ outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
1561
+ }
1562
+ const coinName = token.name;
1563
+ const coinSymbol = token.symbol;
1564
+ let amountIn;
1565
+ if (amountMode === "usd") {
1566
+ const usdVal = parsePercentageLikeValue(opts.usd);
1567
+ if (usdVal === void 0 || usdVal <= 0) {
1568
+ outputErrorAndExit(
1569
+ json,
1570
+ "Invalid --usd value. Must be a positive number."
1571
+ );
1572
+ return;
1573
+ }
1574
+ let priceUsd;
1575
+ if (inputToken.fixedPriceUsd != null) {
1576
+ priceUsd = inputToken.fixedPriceUsd;
1577
+ } else {
1578
+ const fetched = await fetchTokenPriceUsd(inputToken.priceAddress);
1579
+ if (fetched === null) {
1580
+ outputErrorAndExit(
1581
+ json,
1582
+ `Failed to fetch ${inputToken.symbol} price.`
1583
+ );
1584
+ return;
1585
+ }
1586
+ priceUsd = fetched;
1587
+ }
1588
+ const tokenAmount = usdVal / priceUsd;
1589
+ amountIn = parseUnits(
1590
+ tokenAmount.toFixed(inputToken.decimals),
1591
+ inputToken.decimals
1592
+ );
1593
+ if (amountIn === 0n) {
1594
+ outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
1595
+ }
1596
+ if (debug) {
1597
+ console.error(
1598
+ `[debug] $${usdVal} USD = ${formatUnits3(amountIn, inputToken.decimals)} ${inputToken.symbol} (price: $${priceUsd})`
1599
+ );
1600
+ }
1601
+ } else if (amountMode === "eth") {
1602
+ const val = parsePercentageLikeValue(opts.eth);
1603
+ if (val === void 0 || val <= 0) {
1604
+ outputErrorAndExit(
1605
+ json,
1606
+ "Invalid --eth value. Must be a positive number."
1607
+ );
1608
+ }
1609
+ try {
1610
+ amountIn = parseUnits(opts.eth, inputToken.decimals);
1611
+ } catch {
1612
+ outputErrorAndExit(
1613
+ json,
1614
+ "Invalid --eth value. Must be a positive number."
1615
+ );
1616
+ }
1617
+ } else {
1618
+ const isEth = tokenKey === "eth";
1619
+ let balance;
1620
+ if (isEth) {
1621
+ balance = await publicClient.getBalance({
1622
+ address: account.address
1623
+ });
1624
+ } else {
1625
+ const tokenAddress = inputToken.trade.address;
1626
+ balance = await publicClient.readContract({
1627
+ address: tokenAddress,
1628
+ abi: [
1629
+ {
1630
+ name: "balanceOf",
1631
+ type: "function",
1632
+ stateMutability: "view",
1633
+ inputs: [{ name: "account", type: "address" }],
1634
+ outputs: [{ name: "", type: "uint256" }]
1635
+ }
1636
+ ],
1637
+ functionName: "balanceOf",
1638
+ args: [account.address]
1639
+ });
1640
+ }
1641
+ if (balance === 0n) {
1642
+ outputErrorAndExit(
1643
+ json,
1644
+ `No ${inputToken.symbol} balance. Deposit ${inputToken.symbol} to ${account.address} on Base.`
1645
+ );
1646
+ }
1647
+ const gasReserve = isEth ? GAS_RESERVE : 0n;
1648
+ if (isEth && balance <= gasReserve) {
1649
+ outputErrorAndExit(
1650
+ json,
1651
+ `Balance too low (${formatEthDisplay(balance)} ETH). Need >0.001 ETH for gas.`
1652
+ );
1653
+ }
1654
+ const spendableBalance = balance - gasReserve;
1655
+ if (amountMode === "all") {
1656
+ amountIn = spendableBalance;
1657
+ } else {
1658
+ const pct = parsePercentageLikeValue(opts.percent);
1659
+ if (pct === void 0 || pct <= 0 || pct > 100) {
1660
+ outputErrorAndExit(
1661
+ json,
1662
+ "Invalid --percent value. Must be between 0 and 100."
1663
+ );
1664
+ }
1665
+ amountIn = pct === 100 ? spendableBalance : balance * BigInt(Math.round(pct * 100)) / 10000n;
1666
+ if (amountIn === 0n) {
1667
+ outputErrorAndExit(
1668
+ json,
1669
+ "Calculated amount is zero. Balance too low."
1670
+ );
1671
+ }
1672
+ }
1673
+ }
1674
+ const tradeParameters = {
1675
+ sell: inputToken.trade,
1676
+ buy: { type: "erc20", address: coinAddress },
1677
+ amountIn,
1678
+ slippage,
1679
+ sender: account.address
1680
+ };
1681
+ if (debug) {
1682
+ printDebugRequest("buy", tradeParameters);
1683
+ }
1684
+ let amountOut;
1685
+ try {
1686
+ const quote = await createTradeCall(tradeParameters);
1687
+ if (debug) {
1688
+ printDebugResponse("buy", quote);
1689
+ }
1690
+ if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
1691
+ outputErrorAndExit(
1692
+ json,
1693
+ "Quote returned zero output. Amount may be too small."
1694
+ );
1695
+ }
1696
+ amountOut = quote.quote.amountOut;
1697
+ } catch (err) {
1698
+ if (debug) {
1699
+ console.error(
1700
+ `
1701
+ [debug] buy \u2014 Quote Error:
1702
+ ${err instanceof Error ? err.stack || err.message : String(err)}
1703
+ `
1704
+ );
1705
+ }
1706
+ const msg = err instanceof Error ? err.message : String(err);
1707
+ const errorType = err?.errorType;
1708
+ const errorBody = err?.errorBody;
1709
+ if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
1710
+ if (json) {
1711
+ outputJson({ error: errorBody ?? msg });
1712
+ process.exit(1);
1713
+ }
1714
+ outputErrorAndExit(
1715
+ json,
1716
+ "Not enough available liquidity for your swap. Please try swapping fewer tokens."
1717
+ );
1718
+ }
1719
+ outputErrorAndExit(
1720
+ json,
1721
+ `Quote failed: ${msg}`,
1722
+ "Check the coin address is valid and try again. Use --debug for full error details."
1723
+ );
1724
+ }
1725
+ const spendAmount = formatUnits3(amountIn, inputToken.decimals);
1726
+ const spendFormatted = new Intl.NumberFormat("en-US", {
1727
+ maximumFractionDigits: 6
1728
+ }).format(Number(spendAmount));
1729
+ const coinsOut = formatUnits3(BigInt(amountOut), 18);
1730
+ const coinsFormatted = formatCoinsDisplay(coinsOut);
1731
+ if (opts.quote) {
1732
+ printQuote(json, {
1733
+ coinName,
1734
+ coinSymbol,
1735
+ address: coinAddress,
1736
+ spendAmount: `${spendFormatted} ${inputToken.symbol}`,
1737
+ amountIn,
1738
+ inputTokenSymbol: inputToken.symbol,
1739
+ inputTokenDecimals: inputToken.decimals,
1740
+ coinsFormatted,
1741
+ amountOut,
1742
+ slippagePct
1743
+ });
1744
+ track("cli_buy", {
1745
+ action: "quote",
1746
+ coin_address: coinAddress,
1747
+ coin_name: coinName,
1748
+ coin_symbol: coinSymbol,
1749
+ amount_mode: amountMode,
1750
+ slippage: slippagePct,
1751
+ output_format: opts.output
1752
+ });
1753
+ return;
1754
+ }
1755
+ if (!opts.yes) {
1756
+ printQuote(false, {
1757
+ coinName,
1758
+ coinSymbol,
1759
+ address: coinAddress,
1760
+ spendAmount: `${spendFormatted} ${inputToken.symbol}`,
1761
+ amountIn,
1762
+ inputTokenSymbol: inputToken.symbol,
1763
+ inputTokenDecimals: inputToken.decimals,
1764
+ coinsFormatted,
1765
+ amountOut,
1766
+ slippagePct
1767
+ });
1768
+ const ok = await confirm2({
1769
+ message: "Confirm?",
1770
+ default: false
1771
+ });
1772
+ if (!ok) {
1773
+ process.exit(0);
1774
+ }
1775
+ }
1776
+ let receipt;
1777
+ let txHash;
1778
+ let receivedAmountOut = BigInt(amountOut);
1779
+ try {
1780
+ receipt = await tradeCoin({
1781
+ tradeParameters,
1782
+ walletClient,
1783
+ publicClient,
1784
+ account
1785
+ });
1786
+ } catch (err) {
1787
+ track("cli_buy", {
1788
+ action: "trade",
1789
+ coin_address: coinAddress,
1790
+ coin_name: coinName,
1791
+ coin_symbol: coinSymbol,
1792
+ amount_mode: amountMode,
1793
+ slippage: slippagePct,
1794
+ output_format: opts.output,
1795
+ success: false,
1796
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
1797
+ });
1798
+ await shutdownAnalytics();
1799
+ outputErrorAndExit(
1800
+ json,
1801
+ `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
1802
+ );
1803
+ }
1804
+ txHash = receipt.transactionHash;
1805
+ try {
1806
+ receivedAmountOut = getReceivedAmountFromReceipt({
1807
+ receipt,
1808
+ tokenAddress: coinAddress,
1809
+ recipient: account.address
1810
+ });
1811
+ } catch (err) {
1812
+ console.warn(
1813
+ `Warning: transaction succeeded but could not determine received amount: ${err instanceof Error ? err.message : String(err)}`
1814
+ );
1815
+ console.warn(`Tx: ${txHash}`);
1816
+ }
1817
+ printTradeResult(json, {
1818
+ coinName,
1819
+ coinSymbol,
1820
+ address: coinAddress,
1821
+ spendAmount,
1822
+ amountIn,
1823
+ inputTokenSymbol: inputToken.symbol,
1824
+ inputTokenDecimals: inputToken.decimals,
1825
+ receivedAmountOut,
1826
+ txHash
1827
+ });
1828
+ track("cli_buy", {
1829
+ action: "trade",
1830
+ coin_address: coinAddress,
1831
+ coin_name: coinName,
1832
+ coin_symbol: coinSymbol,
1833
+ amount_mode: amountMode,
1834
+ input_amount: amountIn.toString(),
1835
+ input_token_symbol: inputToken.symbol,
1836
+ slippage: slippagePct,
1837
+ output_format: opts.output,
1838
+ success: true,
1839
+ tx_hash: txHash
1840
+ });
1841
+ });
1842
+
1843
+ // src/commands/get.tsx
1844
+ import { Command as Command5 } from "commander";
1845
+ import { setApiKey as setApiKey4 } from "@zoralabs/coins-sdk";
1846
+
1847
+ // src/lib/coin-ref.ts
1848
+ import { getCoin as getCoin2, getProfile, getTrend } from "@zoralabs/coins-sdk";
1849
+ var COIN_TYPE_MAP = {
1850
+ CONTENT: "post",
1851
+ CREATOR: "creator-coin",
1852
+ TREND: "trend"
1853
+ };
1854
+ function mapCoinType(raw) {
1855
+ if (!raw) return "unknown";
1856
+ return COIN_TYPE_MAP[raw] ?? "unknown";
1857
+ }
1858
+ function coinFromToken(token) {
1859
+ return {
1860
+ name: token.name ?? "Unknown",
1861
+ address: token.address ?? "",
1862
+ coinType: mapCoinType(token.coinType),
1863
+ marketCap: token.marketCap ?? "0",
1864
+ marketCapDelta24h: token.marketCapDelta24h ?? "0",
1865
+ volume24h: token.volume24h ?? "0",
1866
+ uniqueHolders: token.uniqueHolders ?? 0,
1867
+ createdAt: token.createdAt,
1868
+ creatorAddress: token.creatorAddress,
1869
+ creatorHandle: token.creatorProfile?.handle
1870
+ };
1871
+ }
1872
+ function parseCoinRef(identifier, type) {
1873
+ if (identifier.startsWith("0x")) {
1874
+ return { kind: "address", address: identifier };
1875
+ }
1876
+ if (type === "creator-coin") {
1877
+ return { kind: "prefixed", type: "creator-coin", name: identifier };
1878
+ }
1879
+ if (type === "trend") {
1880
+ return { kind: "prefixed", type: "trend", name: identifier };
1881
+ }
1882
+ return { kind: "ambiguous", name: identifier };
1883
+ }
1884
+ async function resolveByAddress(address) {
1885
+ const response = await getCoin2({ address });
1886
+ if (response.error || !response.data?.zora20Token) {
1887
+ return {
1888
+ kind: "not-found",
1889
+ message: `No coin found at address ${address}`
1890
+ };
1891
+ }
1892
+ return { kind: "found", coin: coinFromToken(response.data.zora20Token) };
1893
+ }
1894
+ async function resolveByTrendTicker(ticker) {
1895
+ const response = await getTrend({ ticker });
1896
+ if (response.error || !response.data?.trendCoin) {
1897
+ return {
1898
+ kind: "not-found",
1899
+ message: `No trend coin found with ticker "${ticker}"`
1900
+ };
1901
+ }
1902
+ return { kind: "found", coin: coinFromToken(response.data.trendCoin) };
1903
+ }
1904
+ async function resolveByCreatorName(name) {
1905
+ const response = await getProfile({ identifier: name });
1906
+ if (response.error || !response.data?.profile) {
1907
+ return {
1908
+ kind: "not-found",
1909
+ message: `No creator found with name "${name}"`
1910
+ };
1911
+ }
1912
+ const profile = response.data.profile;
1913
+ if (!profile.creatorCoin) {
1914
+ return {
1915
+ kind: "not-found",
1916
+ message: `"${name}" does not have a creator coin`
1917
+ };
1918
+ }
1919
+ return resolveByAddress(profile.creatorCoin.address);
1920
+ }
1921
+ async function resolveCoin(ref) {
1922
+ switch (ref.kind) {
1923
+ case "address":
1924
+ return resolveByAddress(ref.address);
1925
+ case "prefixed":
1926
+ if (ref.type === "trend") {
1927
+ return resolveByTrendTicker(ref.name);
1928
+ }
1929
+ return resolveByCreatorName(ref.name);
1930
+ case "ambiguous":
1931
+ return resolveByCreatorName(ref.name);
1932
+ }
1933
+ }
1934
+
1935
+ // src/components/CoinDetail.tsx
1936
+ import { Box as Box3, Text as Text3 } from "ink";
1937
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1938
+ var LABEL_WIDTH = 18;
1939
+ function Row({
1940
+ label,
1941
+ children
1942
+ }) {
1943
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1944
+ /* @__PURE__ */ jsx3(Box3, { width: LABEL_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label }) }),
1945
+ /* @__PURE__ */ jsx3(Text3, { children })
1946
+ ] });
1947
+ }
1948
+ function CoinDetail({ coin }) {
1949
+ const change = formatMcapChange(coin.marketCap, coin.marketCapDelta24h);
1950
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingLeft: 1, children: [
1951
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1952
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: coin.name }),
1953
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1954
+ coin.coinType,
1955
+ " ",
1956
+ "\xB7",
1957
+ " ",
1958
+ coin.address
1959
+ ] })
1960
+ ] }),
1961
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1962
+ /* @__PURE__ */ jsx3(Row, { label: "Market Cap", children: formatCurrency(coin.marketCap) }),
1963
+ /* @__PURE__ */ jsx3(Row, { label: "24h Volume", children: formatCurrency(coin.volume24h) }),
1964
+ /* @__PURE__ */ jsx3(Row, { label: "24h Change", children: /* @__PURE__ */ jsx3(Text3, { color: change.color, children: change.text }) }),
1965
+ /* @__PURE__ */ jsx3(Row, { label: "Holders", children: formatHolders(coin.uniqueHolders) }),
1966
+ coin.coinType === "post" && (coin.creatorHandle ?? coin.creatorAddress) && /* @__PURE__ */ jsx3(Row, { label: "Creator", children: coin.creatorHandle ?? coin.creatorAddress }),
1967
+ /* @__PURE__ */ jsx3(Row, { label: "Created", children: formatCreatedAt(coin.createdAt) })
1968
+ ] }),
1969
+ /* @__PURE__ */ jsx3(Box3, { marginBottom: 1 })
1970
+ ] });
1971
+ }
1972
+
1973
+ // src/commands/get.tsx
1974
+ import { jsx as jsx4 } from "react/jsx-runtime";
1975
+ function formatCoinJson(coin) {
1976
+ return {
1977
+ name: coin.name,
1978
+ address: coin.address,
1979
+ coinType: coin.coinType,
1980
+ marketCap: coin.marketCap,
1981
+ marketCapDelta24h: coin.marketCapDelta24h,
1982
+ volume24h: coin.volume24h,
1983
+ uniqueHolders: coin.uniqueHolders,
1984
+ createdAt: coin.createdAt ?? null,
1985
+ creatorAddress: coin.creatorAddress ?? null,
1986
+ creatorHandle: coin.creatorHandle ?? null
1987
+ };
1988
+ }
1989
+ var VALID_TYPES = ["creator-coin", "post", "trend"];
1990
+ var getCommand = new Command5("get").description("Look up a coin by address or name").argument("<identifier>", "Coin address (0x...) or creator name").option("--type <type>", "Coin type: creator-coin, post, trend").action(async function(identifier, opts) {
1991
+ const json = getJson(this);
1992
+ if (opts.type !== void 0 && !VALID_TYPES.includes(opts.type)) {
1993
+ outputErrorAndExit(
1994
+ json,
1995
+ `Invalid --type value: ${opts.type}.`,
1996
+ `Supported: ${VALID_TYPES.join(", ")}`
1997
+ );
1998
+ }
1999
+ const type = opts.type;
2000
+ if (type === "post" && !identifier.startsWith("0x")) {
2001
+ outputErrorAndExit(
2002
+ json,
2003
+ "Posts can only be looked up by address.",
2004
+ "Use: zora get 0x..."
2005
+ );
2006
+ }
2007
+ const ref = parseCoinRef(identifier, opts.type);
2008
+ const apiKey = getApiKey();
2009
+ if (apiKey) {
2010
+ setApiKey4(apiKey);
2011
+ }
2012
+ let result;
2013
+ try {
2014
+ result = await resolveCoin(ref);
2015
+ } catch (err) {
2016
+ outputErrorAndExit(
2017
+ json,
2018
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
2019
+ );
2020
+ return;
2021
+ }
2022
+ if (type && result.kind === "found" && result.coin.coinType !== type) {
2023
+ outputErrorAndExit(
2024
+ json,
2025
+ `Coin at ${result.coin.address} is a ${result.coin.coinType}, not a ${type}.`,
2026
+ `Use: zora get ${result.coin.address} --type ${result.coin.coinType}`
2027
+ );
2028
+ return;
2029
+ }
2030
+ if (result.kind === "not-found") {
2031
+ outputErrorAndExit(json, result.message);
2032
+ return;
2033
+ }
2034
+ outputData(json, {
2035
+ json: formatCoinJson(result.coin),
2036
+ table: () => {
2037
+ renderOnce(/* @__PURE__ */ jsx4(CoinDetail, { coin: result.coin }));
2038
+ }
2039
+ });
2040
+ track("cli_get", {
2041
+ lookup_type: identifier.startsWith("0x") ? "address" : "name",
2042
+ coin_type_filter: type ?? null,
2043
+ found: result.kind === "found",
2044
+ coin_type: result.kind === "found" ? result.coin.coinType : null,
2045
+ output_format: json ? "json" : "text"
2046
+ });
2047
+ });
2048
+
2049
+ // src/commands/sell.ts
2050
+ import { Command as Command6 } from "commander";
2051
+ import confirm3 from "@inquirer/confirm";
2052
+ import {
2053
+ erc20Abi as erc20Abi3,
2054
+ formatUnits as formatUnits4,
2055
+ isAddress as isAddress2,
2056
+ parseUnits as parseUnits2
2057
+ } from "viem";
2058
+ import {
2059
+ createTradeCall as createTradeCall2,
2060
+ getCoin as getCoin3,
2061
+ setApiKey as setApiKey5,
2062
+ tradeCoin as tradeCoin2
2063
+ } from "@zoralabs/coins-sdk";
2064
+ function printSellQuote(output, info) {
2065
+ if (output === "json") {
2066
+ outputJson({
2067
+ action: "quote",
2068
+ coin: info.coinSymbol,
2069
+ address: info.address,
2070
+ sell: {
2071
+ amount: formatUnits4(info.amountIn, info.coinDecimals),
2072
+ raw: info.amountIn.toString(),
2073
+ symbol: info.coinSymbol
2074
+ },
2075
+ estimated: {
2076
+ amount: formatUnits4(BigInt(info.quoteAmountOut), info.outputDecimals),
2077
+ raw: info.quoteAmountOut,
2078
+ symbol: info.outputSymbol
2079
+ },
2080
+ slippage: info.slippagePct
2081
+ });
2082
+ return;
2083
+ }
2084
+ console.log(`
2085
+ Sell ${info.coinName} (${info.coinSymbol})
2086
+ `);
2087
+ console.log(` Amount ${info.soldFormatted} ${info.coinSymbol}`);
2088
+ console.log(
2089
+ ` You get ~${info.receivedFormatted} ${info.outputSymbol}`
2090
+ );
2091
+ console.log(` Slippage ${info.slippagePct}%
2092
+ `);
2093
+ }
2094
+ function printSellResult(output, info) {
2095
+ const receivedAmount = formatUnits4(
2096
+ info.receivedAmountOut,
2097
+ info.outputDecimals
2098
+ );
2099
+ const receivedFormatted = formatAmountDisplay(
2100
+ info.receivedAmountOut,
2101
+ info.outputDecimals
2102
+ );
2103
+ if (output === "json") {
2104
+ outputJson({
2105
+ action: "sell",
2106
+ coin: info.coinSymbol,
2107
+ address: info.address,
2108
+ sold: {
2109
+ amount: formatUnits4(info.amountIn, info.coinDecimals),
2110
+ raw: info.amountIn.toString(),
2111
+ symbol: info.coinSymbol
2112
+ },
2113
+ received: {
2114
+ amount: receivedAmount,
2115
+ raw: info.receivedAmountOut.toString(),
2116
+ symbol: info.outputSymbol,
2117
+ source: info.receivedSource
2118
+ },
2119
+ tx: info.txHash
2120
+ });
2121
+ return;
2122
+ }
2123
+ console.log(`
2124
+ Sold ${info.coinName}
2125
+ `);
2126
+ console.log(` Sold ${info.soldFormatted} ${info.coinSymbol}`);
2127
+ console.log(
2128
+ ` Received ${info.receivedSource === "quote" ? "~" : ""}${receivedFormatted} ${info.outputSymbol}`
2129
+ );
2130
+ if (info.receivedSource === "quote") {
2131
+ console.log(" Note based on quote");
2132
+ }
2133
+ console.log(` Tx ${info.txHash}
2134
+ `);
2135
+ }
2136
+ var sellCommand = new Command6("sell").description("Sell a coin").argument("<address>", "Coin contract address (0x\u2026)").option("--amount <value>", "Sell specific number of coins").option("--usd <value>", "Sell USD equivalent worth of coins").option("--percent <value>", "Sell percentage of coin balance").option("--all", "Sell entire coin balance").option("--to <asset>", "Receive asset: eth, usdc, zora", "eth").option("--token <asset>", "Receive asset: eth, usdc, zora (alias for --to)").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").option("-o, --output <format>", "Output format: table, json", "table").action(async (coinAddress, opts) => {
2137
+ const json = opts.output === "json";
2138
+ const debug = opts.debug === true;
2139
+ if (!isAddress2(coinAddress)) {
2140
+ outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
2141
+ }
2142
+ const output = opts.output;
2143
+ if (output !== "table" && output !== "json") {
2144
+ outputErrorAndExit(
2145
+ false,
2146
+ `Invalid --output value: ${output}. Use: table, json`
2147
+ );
2148
+ }
2149
+ const outputAsset = opts.token ? opts.token.toLowerCase() : opts.to;
2150
+ if (!(outputAsset in BASE_TRADE_TOKENS)) {
2151
+ outputErrorAndExit(
2152
+ json,
2153
+ `Invalid --${opts.token ? "token" : "to"} value: ${outputAsset}. Use: eth, usdc, zora`
2154
+ );
2155
+ }
2156
+ const outputToken = BASE_TRADE_TOKENS[outputAsset];
2157
+ const amountMode = getAmountMode(
2158
+ json,
2159
+ opts,
2160
+ SELL_AMOUNT_CHECKS,
2161
+ "--amount, --usd, --percent, or --all"
2162
+ );
2163
+ const slippagePct = parsePercentageLikeValue(opts.slippage);
2164
+ if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
2165
+ outputErrorAndExit(
2166
+ json,
2167
+ "Invalid --slippage value. Must be between 0 and 99."
2168
+ );
2169
+ }
2170
+ const slippage = slippagePct / 100;
2171
+ const apiKey = getApiKey();
2172
+ if (apiKey) {
2173
+ setApiKey5(apiKey);
2174
+ }
2175
+ const account = resolveAccount(json);
2176
+ const { publicClient, walletClient } = createClients(account);
2177
+ let token;
2178
+ try {
2179
+ const response = await getCoin3({ address: coinAddress });
2180
+ token = response.data?.zora20Token;
2181
+ } catch (err) {
2182
+ outputErrorAndExit(
2183
+ json,
2184
+ `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
2185
+ );
2186
+ }
2187
+ if (!token) {
2188
+ outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
2189
+ }
2190
+ const coinName = token.name;
2191
+ const coinSymbol = token.symbol;
2192
+ const coinDecimals = Number(token.decimals ?? 18);
2193
+ let amountIn;
2194
+ if (amountMode === "usd") {
2195
+ const usdVal = parsePercentageLikeValue(opts.usd);
2196
+ if (usdVal === void 0 || usdVal <= 0) {
2197
+ outputErrorAndExit(
2198
+ json,
2199
+ "Invalid --usd value. Must be a positive number."
2200
+ );
2201
+ return;
2202
+ }
2203
+ const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
2204
+ if (coinPriceUsd === null || coinPriceUsd <= 0) {
2205
+ outputErrorAndExit(
2206
+ json,
2207
+ `Failed to fetch ${coinSymbol} price for USD conversion.`
2208
+ );
2209
+ return;
2210
+ }
2211
+ const coinAmount = usdVal / coinPriceUsd;
2212
+ amountIn = parseUnits2(coinAmount.toFixed(coinDecimals), coinDecimals);
2213
+ if (amountIn === 0n) {
2214
+ outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
2215
+ }
2216
+ if (debug) {
2217
+ console.error(
2218
+ `[debug] $${usdVal} USD = ${formatUnits4(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
2219
+ );
2220
+ }
2221
+ } else if (amountMode === "amount") {
2222
+ const val = parsePercentageLikeValue(opts.amount);
2223
+ if (val === void 0 || val <= 0) {
2224
+ outputErrorAndExit(
2225
+ json,
2226
+ "Invalid --amount value. Must be a positive number."
2227
+ );
2228
+ }
2229
+ try {
2230
+ amountIn = parseUnits2(opts.amount, coinDecimals);
2231
+ } catch {
2232
+ outputErrorAndExit(json, "Invalid --amount value for token decimals.");
2233
+ }
2234
+ } else {
2235
+ const balance = await publicClient.readContract({
2236
+ abi: erc20Abi3,
2237
+ address: coinAddress,
2238
+ functionName: "balanceOf",
2239
+ args: [account.address]
2240
+ });
2241
+ if (balance === 0n) {
2242
+ outputErrorAndExit(
2243
+ json,
2244
+ `No ${coinSymbol} balance. Buy some first or pick a different wallet.`
2245
+ );
2246
+ }
2247
+ if (amountMode === "all") {
2248
+ amountIn = balance;
2249
+ } else {
2250
+ const pct = parsePercentageLikeValue(opts.percent);
2251
+ if (pct === void 0 || pct <= 0 || pct > 100) {
2252
+ outputErrorAndExit(
2253
+ json,
2254
+ "Invalid --percent value. Must be between 0 and 100."
2255
+ );
2256
+ }
2257
+ amountIn = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
2258
+ if (amountIn === 0n) {
2259
+ outputErrorAndExit(
2260
+ json,
2261
+ "Calculated amount is zero. Balance too low."
2262
+ );
2263
+ }
2264
+ }
2265
+ }
2266
+ const tradeParameters = {
2267
+ sell: { type: "erc20", address: coinAddress },
2268
+ buy: outputToken.trade,
2269
+ amountIn,
2270
+ slippage,
2271
+ sender: account.address
2272
+ };
2273
+ if (debug) {
2274
+ printDebugRequest("sell", tradeParameters);
2275
+ }
2276
+ let quoteAmountOut;
2277
+ try {
2278
+ const quote = await createTradeCall2(tradeParameters);
2279
+ if (debug) {
2280
+ printDebugResponse("sell", quote);
2281
+ }
2282
+ if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
2283
+ outputErrorAndExit(
2284
+ json,
2285
+ "Quote returned zero output. Amount may be too small."
2286
+ );
2287
+ }
2288
+ quoteAmountOut = quote.quote.amountOut;
2289
+ } catch (err) {
2290
+ if (debug) {
2291
+ console.error(
2292
+ `
2293
+ [debug] sell \u2014 Quote Error:
2294
+ ${err instanceof Error ? err.stack || err.message : String(err)}
2295
+ `
2296
+ );
2297
+ }
2298
+ const msg = err instanceof Error ? err.message : String(err);
2299
+ const errorType = err?.errorType;
2300
+ const errorBody = err?.errorBody;
2301
+ if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
2302
+ if (json) {
2303
+ outputJson({ error: errorBody ?? msg });
2304
+ process.exit(1);
2305
+ }
2306
+ outputErrorAndExit(
2307
+ json,
2308
+ "Not enough available liquidity for your swap. Please try swapping fewer tokens."
2309
+ );
2310
+ }
2311
+ outputErrorAndExit(
2312
+ json,
2313
+ `Quote failed: ${msg}`,
2314
+ "Check the coin address and amount, then try again. Use --debug for full error details."
2315
+ );
2316
+ }
2317
+ const soldFormatted = formatAmountDisplay(amountIn, coinDecimals);
2318
+ const receivedFormatted = formatAmountDisplay(
2319
+ BigInt(quoteAmountOut),
2320
+ outputToken.decimals
2321
+ );
2322
+ if (opts.quote) {
2323
+ printSellQuote(output, {
2324
+ coinName,
2325
+ coinSymbol,
2326
+ address: coinAddress,
2327
+ soldFormatted,
2328
+ amountIn,
2329
+ coinDecimals,
2330
+ receivedFormatted,
2331
+ quoteAmountOut,
2332
+ outputSymbol: outputToken.symbol,
2333
+ outputDecimals: outputToken.decimals,
2334
+ slippagePct
2335
+ });
2336
+ track("cli_sell", {
2337
+ action: "quote",
2338
+ coin_address: coinAddress,
2339
+ coin_name: coinName,
2340
+ coin_symbol: coinSymbol,
2341
+ amount_mode: amountMode,
2342
+ output_asset: outputAsset,
2343
+ slippage: slippagePct,
2344
+ output_format: output
2345
+ });
2346
+ return;
2347
+ }
2348
+ if (!opts.yes) {
2349
+ printSellQuote("table", {
2350
+ coinName,
2351
+ coinSymbol,
2352
+ address: coinAddress,
2353
+ soldFormatted,
2354
+ amountIn,
2355
+ coinDecimals,
2356
+ receivedFormatted,
2357
+ quoteAmountOut,
2358
+ outputSymbol: outputToken.symbol,
2359
+ outputDecimals: outputToken.decimals,
2360
+ slippagePct
2361
+ });
2362
+ const ok = await confirm3({
2363
+ message: "Confirm?",
2364
+ default: false
2365
+ });
2366
+ if (!ok) {
2367
+ console.error("Aborted.");
2368
+ process.exit(0);
2369
+ }
2370
+ }
2371
+ let receipt;
2372
+ let txHash;
2373
+ let receivedAmountOut = BigInt(quoteAmountOut);
2374
+ let receivedSource = "quote";
2375
+ try {
2376
+ receipt = await tradeCoin2({
2377
+ tradeParameters,
2378
+ walletClient,
2379
+ publicClient,
2380
+ account
2381
+ });
2382
+ } catch (err) {
2383
+ track("cli_sell", {
2384
+ action: "trade",
2385
+ coin_address: coinAddress,
2386
+ coin_name: coinName,
2387
+ coin_symbol: coinSymbol,
2388
+ amount_mode: amountMode,
2389
+ output_asset: outputAsset,
2390
+ slippage: slippagePct,
2391
+ output_format: output,
2392
+ success: false,
2393
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
2394
+ });
2395
+ await shutdownAnalytics();
2396
+ outputErrorAndExit(
2397
+ json,
2398
+ `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
2399
+ );
2400
+ }
2401
+ txHash = receipt.transactionHash;
2402
+ if (outputToken.trade.type === "erc20") {
2403
+ try {
2404
+ receivedAmountOut = getReceivedAmountFromReceipt({
2405
+ receipt,
2406
+ tokenAddress: outputToken.trade.address,
2407
+ recipient: account.address
2408
+ });
2409
+ receivedSource = "receipt";
2410
+ } catch {
2411
+ }
2412
+ }
2413
+ printSellResult(output, {
2414
+ coinName,
2415
+ coinSymbol,
2416
+ address: coinAddress,
2417
+ amountIn,
2418
+ coinDecimals,
2419
+ soldFormatted,
2420
+ receivedAmountOut,
2421
+ outputSymbol: outputToken.symbol,
2422
+ outputDecimals: outputToken.decimals,
2423
+ receivedSource,
2424
+ txHash
2425
+ });
2426
+ track("cli_sell", {
2427
+ action: "trade",
2428
+ coin_address: coinAddress,
2429
+ coin_name: coinName,
2430
+ coin_symbol: coinSymbol,
2431
+ amount_mode: amountMode,
2432
+ output_asset: outputAsset,
2433
+ slippage: slippagePct,
2434
+ output_format: output,
2435
+ success: true,
2436
+ tx_hash: txHash
2437
+ });
2438
+ });
2439
+
2440
+ // src/commands/setup.ts
2441
+ import { Command as Command7 } from "commander";
2442
+ import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2443
+
2444
+ // src/lib/strings.ts
2445
+ var DEPOSIT_INSTRUCTIONS = "Deposit ETH or USDC to this address on Base to start trading.\n\n You can do this from:\n - Coinbase \u2014 withdraw directly to Base\n - Another wallet (MetaMask, Rainbow, etc.) \u2014 send on Base network\n - Bridge from other chains \u2014 use https://superbridge.app/base";
2446
+ var NO_WALLET_CONFIGURED = "No wallet configured.";
2447
+ var NO_WALLET_SUGGESTION = "Run 'zora setup' to create or import one.";
2448
+ var SAVE_ERROR_HINT = "Check that the directory exists and is writable.";
2449
+ var BACKUP_WARNING = "Back up this file \u2014 it's the only copy of your key.";
2450
+
2451
+ // src/commands/setup.ts
2452
+ var isValidPrivateKey = (key) => /^(0x)?[0-9a-fA-F]{64}$/.test(key);
2453
+ var toAccount = (json, key, errorPrefix) => {
2454
+ try {
2455
+ return privateKeyToAccount3(normalizeKey(key));
2456
+ } catch {
2457
+ outputErrorAndExit(
2458
+ json,
2459
+ `\u2717 ${errorPrefix} isn't a valid private key.`
2460
+ );
2461
+ }
2462
+ };
2463
+ var setupCommand = new Command7("setup").description("Set up your Zora wallet").option("--create", "Create a new wallet without prompting").option("--force", "Overwrite existing wallet without prompting").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
2464
+ const json = getJson(this);
2465
+ const nonInteractive = getYes(this);
2466
+ const envKey = process.env.ZORA_PRIVATE_KEY;
2467
+ if (envKey !== void 0) {
2468
+ if (!isValidPrivateKey(envKey)) {
2469
+ outputErrorAndExit(
2470
+ json,
2471
+ "\u2717 ZORA_PRIVATE_KEY isn't a valid private key.",
2472
+ "Fix it and run zora setup again."
2473
+ );
2474
+ }
2475
+ const account = toAccount(json, envKey, "ZORA_PRIVATE_KEY");
2476
+ outputData(json, {
2477
+ json: { source: "env", address: account.address },
2478
+ table: () => {
2479
+ console.log(" Using wallet from ZORA_PRIVATE_KEY.\n");
2480
+ console.log(` Address: ${account.address}
2481
+ `);
2482
+ console.log(` ${DEPOSIT_INSTRUCTIONS}`);
2483
+ }
2484
+ });
2485
+ track("cli_setup", {
2486
+ action: "env_detected",
2487
+ source: "env",
2488
+ output_format: json ? "json" : "text"
2489
+ });
2490
+ return;
2491
+ }
2492
+ let existing;
2493
+ if (!options.force) {
2494
+ try {
2495
+ existing = getPrivateKey();
2496
+ } catch (err) {
2497
+ outputErrorAndExit(
2498
+ json,
2499
+ `\u2717 Could not read wallet: ${err.message}`,
2500
+ "Run 'zora setup --force' to overwrite it."
2501
+ );
2502
+ }
2503
+ }
2504
+ if (existing) {
2505
+ const account = toAccount(json, existing, "Stored private key");
2506
+ const truncated = `${account.address.slice(0, 6)}\u2026${account.address.slice(-4)}`;
2507
+ console.log(` Wallet already configured: ${truncated}
2508
+ `);
2509
+ if (!options.force) {
2510
+ outputErrorAndExit(
2511
+ json,
2512
+ "Wallet already exists.",
2513
+ "Use --force to overwrite."
2514
+ );
2515
+ }
2516
+ }
2517
+ let choice;
2518
+ if (options.create) {
2519
+ choice = "create";
2520
+ } else {
2521
+ choice = await selectOrDefault(
2522
+ {
2523
+ message: "How do you want to set up your wallet?",
2524
+ choices: [
2525
+ {
2526
+ name: "Create a new wallet (recommended)",
2527
+ value: "create"
2528
+ },
2529
+ { name: "Import a private key", value: "import" }
2530
+ ],
2531
+ default: "create"
2532
+ },
2533
+ nonInteractive
2534
+ );
2535
+ }
2536
+ if (choice === "import") {
2537
+ let importedKey;
2538
+ while (!importedKey) {
2539
+ const input = await passwordOrFail(
2540
+ json,
2541
+ { message: "Paste your private key:" },
2542
+ nonInteractive
2543
+ );
2544
+ if (isValidPrivateKey(input.trim())) {
2545
+ importedKey = input.trim();
2546
+ } else {
2547
+ console.error(
2548
+ "\u2717 Not a valid private key. Must be 64 hex characters, with or without a 0x prefix.\n"
2549
+ );
2550
+ }
2551
+ }
2552
+ const account = toAccount(json, importedKey, "Imported key");
2553
+ try {
2554
+ savePrivateKey(importedKey);
2555
+ } catch {
2556
+ outputErrorAndExit(
2557
+ json,
2558
+ `\u2717 Couldn't save to ${getWalletPath()}.`,
2559
+ SAVE_ERROR_HINT
2560
+ );
2561
+ }
2562
+ outputData(json, {
2563
+ json: {
2564
+ action: "imported",
2565
+ address: account.address,
2566
+ path: getWalletPath()
2567
+ },
2568
+ table: () => {
2569
+ console.log("\n\u2713 Wallet imported\n");
2570
+ console.log(` Address: ${account.address}`);
2571
+ console.log(` Private key: saved to ${getWalletPath()}
2572
+ `);
2573
+ console.log(` ${BACKUP_WARNING}
2574
+ `);
2575
+ console.log(` ${DEPOSIT_INSTRUCTIONS}`);
2576
+ }
2577
+ });
2578
+ track("cli_setup", {
2579
+ action: "imported",
2580
+ source: "file",
2581
+ output_format: json ? "json" : "text"
2582
+ });
2583
+ return;
2584
+ }
2585
+ if (choice === "create") {
2586
+ const privateKey = generatePrivateKey();
2587
+ const account = toAccount(json, privateKey, "Generated key");
2588
+ try {
2589
+ savePrivateKey(privateKey);
2590
+ } catch {
2591
+ outputErrorAndExit(
2592
+ json,
2593
+ `\u2717 Couldn't save to ${getWalletPath()}.`,
2594
+ SAVE_ERROR_HINT
2595
+ );
2596
+ }
2597
+ outputData(json, {
2598
+ json: {
2599
+ action: "created",
2600
+ address: account.address,
2601
+ path: getWalletPath()
2602
+ },
2603
+ table: () => {
2604
+ console.log("\n\u2713 Wallet created\n");
2605
+ console.log(` Address: ${account.address}`);
2606
+ console.log(` Private key: saved to ${getWalletPath()}
2607
+ `);
2608
+ console.log(` ${BACKUP_WARNING}
2609
+ `);
2610
+ console.log(` ${DEPOSIT_INSTRUCTIONS}`);
2611
+ }
2612
+ });
2613
+ track("cli_setup", {
2614
+ action: "created",
2615
+ source: "file",
2616
+ output_format: json ? "json" : "text"
2617
+ });
2618
+ }
2619
+ });
2620
+
2621
+ // src/commands/wallet.ts
2622
+ import { Command as Command8 } from "commander";
2623
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2624
+ var resolvePrivateKey = () => {
2625
+ const envKey = process.env.ZORA_PRIVATE_KEY;
2626
+ if (envKey) {
2627
+ return { key: envKey, source: "env" };
2628
+ }
2629
+ const fileKey = getPrivateKey();
2630
+ if (fileKey !== void 0) {
2631
+ return { key: fileKey, source: "file" };
2632
+ }
2633
+ return void 0;
2634
+ };
2635
+ var walletCommand = new Command8("wallet").description(
2636
+ "Manage your Zora wallet"
2637
+ );
2638
+ walletCommand.command("info").description("Show wallet address and storage location").action(function() {
2639
+ const json = getJson(this);
2640
+ const resolved = resolvePrivateKey();
2641
+ if (!resolved) {
2642
+ outputErrorAndExit(json, NO_WALLET_CONFIGURED, NO_WALLET_SUGGESTION);
2643
+ }
2644
+ let account;
2645
+ try {
2646
+ account = privateKeyToAccount4(normalizeKey(resolved.key));
2647
+ } catch {
2648
+ const msg = resolved.source === "env" ? "ZORA_PRIVATE_KEY is not a valid private key." : "Stored private key is invalid.";
2649
+ const suggestion = resolved.source === "env" ? void 0 : "Run 'zora setup --force' to replace it.";
2650
+ outputErrorAndExit(json, `\u2717 ${msg}`, suggestion);
2651
+ }
2652
+ const source = resolved.source === "env" ? "env (ZORA_PRIVATE_KEY)" : getWalletPath();
2653
+ outputData(json, {
2654
+ json: { address: account.address, source },
2655
+ table: () => {
2656
+ console.log(` Address: ${account.address}`);
2657
+ console.log(` Source: ${source}`);
2658
+ }
2659
+ });
2660
+ track("cli_wallet_info", {
2661
+ source: resolved.source,
2662
+ output_format: json ? "json" : "text"
2663
+ });
2664
+ });
2665
+ walletCommand.command("export").description("Print the raw private key to stdout").option("--force", "Skip the confirmation prompt").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
2666
+ const json = getJson(this);
2667
+ const nonInteractive = getYes(this);
2668
+ const resolved = resolvePrivateKey();
2669
+ if (!resolved) {
2670
+ outputErrorAndExit(json, NO_WALLET_CONFIGURED, NO_WALLET_SUGGESTION);
2671
+ }
2672
+ if (!options.force) {
2673
+ console.log(
2674
+ " \u26A0 Your private key grants full access to your wallet."
2675
+ );
2676
+ console.log(
2677
+ " Anyone who sees it can steal your funds. Never share it.\n"
2678
+ );
2679
+ const ok = await confirmOrDefault(
2680
+ { message: "Export private key?", default: false },
2681
+ nonInteractive
2682
+ );
2683
+ if (!ok) {
2684
+ console.error("Aborted.");
2685
+ process.exit(0);
2686
+ }
2687
+ }
2688
+ console.log(resolved.key);
2689
+ track("cli_wallet_export", {
2690
+ output_format: json ? "json" : "text"
2691
+ });
2692
+ });
2693
+
2694
+ // src/components/Zorb.tsx
2695
+ import { Text as Text4, Box as Box4 } from "ink";
2696
+
2697
+ // src/lib/zorb-pixels.ts
2698
+ function supportsTruecolor() {
2699
+ if (!process.stdout.isTTY) return false;
2700
+ const ct = process.env.COLORTERM;
2701
+ if (ct === "truecolor" || ct === "24bit") return true;
2702
+ if (typeof process.stdout.getColorDepth === "function") {
2703
+ return process.stdout.getColorDepth() >= 24;
2704
+ }
2705
+ return false;
2706
+ }
2707
+ function hexToRgb(hex) {
2708
+ const n = parseInt(hex.replace("#", ""), 16);
2709
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
2710
+ }
2711
+ function lerp(a, b, t) {
2712
+ return a + (b - a) * t;
2713
+ }
2714
+ function clamp(v, min, max) {
2715
+ return v < min ? min : v > max ? max : v;
2716
+ }
2717
+ function alphaOver(bg, fg, a) {
2718
+ return [lerp(bg[0], fg[0], a), lerp(bg[1], fg[1], a), lerp(bg[2], fg[2], a)];
2719
+ }
2720
+ function gaussian(dist, sigma) {
2721
+ if (sigma <= 0) return dist <= 0 ? 1 : 0;
2722
+ return Math.exp(-(dist * dist) / (2 * sigma * sigma));
2723
+ }
2724
+ var BASE_COLOR = hexToRgb("#A1723A");
2725
+ var LAYERS = [
2726
+ // 1: Dark maroon shadow
2727
+ {
2728
+ cx: 0.54,
2729
+ cy: 0.45,
2730
+ radius: 0.53,
2731
+ color: hexToRgb("#531002"),
2732
+ blur: 0.062,
2733
+ opacity: 1
2734
+ },
2735
+ // 2: Blue body
2736
+ {
2737
+ cx: 0.6,
2738
+ cy: 0.38,
2739
+ radius: 0.43,
2740
+ color: hexToRgb("#2B5DF0"),
2741
+ blur: 0.124,
2742
+ opacity: 1
2743
+ },
2744
+ // 3: Blue accent (gradient from center color to transparent)
2745
+ {
2746
+ cx: 0.59,
2747
+ cy: 0.38,
2748
+ radius: 0.45,
2749
+ color: hexToRgb("#387AFA"),
2750
+ blur: 0.046,
2751
+ opacity: 1,
2752
+ gradient: { gcx: 0.66, gcy: 0.26 }
2753
+ },
2754
+ // 4: Pink glow
2755
+ {
2756
+ cx: 0.66,
2757
+ cy: 0.27,
2758
+ radius: 0.23,
2759
+ color: hexToRgb("#FCB8D4"),
2760
+ blur: 0.093,
2761
+ opacity: 1
2762
+ },
2763
+ // 5: White specular
2764
+ {
2765
+ cx: 0.66,
2766
+ cy: 0.27,
2767
+ radius: 0.09,
2768
+ color: hexToRgb("#FFFFFF"),
2769
+ blur: 0.062,
2770
+ opacity: 1
2771
+ },
2772
+ // 6: Dark ring (transparent → black → transparent, opacity 0.9)
2773
+ {
2774
+ cx: 0.6,
2775
+ cy: 0.36,
2776
+ radius: 0.82,
2777
+ color: [0, 0, 0],
2778
+ blur: 0.046,
2779
+ opacity: 0.9,
2780
+ ring: true
2781
+ }
2782
+ ];
2783
+ function computeLayerAlpha(nx, ny, layer) {
2784
+ const dx = nx - layer.cx;
2785
+ const dy = ny - layer.cy;
2786
+ const dist = Math.sqrt(dx * dx + dy * dy);
2787
+ const radialFalloff = gaussian(Math.max(0, dist - layer.radius), layer.blur);
2788
+ if (layer.ring) {
2789
+ const normalizedDist = dist / layer.radius;
2790
+ const ringProfile = gaussian(normalizedDist - 0.7, 0.15) * radialFalloff;
2791
+ return { alpha: ringProfile * layer.opacity, color: layer.color };
2792
+ }
2793
+ if (layer.gradient) {
2794
+ const gdx = nx - layer.gradient.gcx;
2795
+ const gdy = ny - layer.gradient.gcy;
2796
+ const gDist = Math.sqrt(gdx * gdx + gdy * gdy);
2797
+ const gradientT = clamp(gDist / (layer.radius * 1.2), 0, 1);
2798
+ const alpha = radialFalloff * (1 - gradientT);
2799
+ return { alpha: alpha * layer.opacity, color: layer.color };
2800
+ }
2801
+ return { alpha: radialFalloff * layer.opacity, color: layer.color };
2802
+ }
2803
+ function circleAlpha(px, py, size) {
2804
+ const cx = (size - 1) / 2;
2805
+ const cy = (size - 1) / 2;
2806
+ const r = size / 2;
2807
+ const dx = px - cx;
2808
+ const dy = py - cy;
2809
+ const dist = Math.sqrt(dx * dx + dy * dy);
2810
+ return clamp(r - dist + 0.5, 0, 1);
2811
+ }
2812
+ function generateZorbPixels(size) {
2813
+ const grid = [];
2814
+ for (let y = 0; y < size; y++) {
2815
+ const row = [];
2816
+ for (let x = 0; x < size; x++) {
2817
+ const nx = x / (size - 1);
2818
+ const ny = y / (size - 1);
2819
+ let pixel = [...BASE_COLOR];
2820
+ for (const layer of LAYERS) {
2821
+ const { alpha, color } = computeLayerAlpha(nx, ny, layer);
2822
+ if (alpha > 1e-3) {
2823
+ pixel = alphaOver(pixel, color, alpha);
2824
+ }
2825
+ }
2826
+ const ca = circleAlpha(x, y, size);
2827
+ pixel = [
2828
+ Math.round(pixel[0] * ca),
2829
+ Math.round(pixel[1] * ca),
2830
+ Math.round(pixel[2] * ca)
2831
+ ];
2832
+ row.push(pixel);
2833
+ }
2834
+ grid.push(row);
2835
+ }
2836
+ return grid;
2837
+ }
2838
+
2839
+ // src/components/Zorb.tsx
2840
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2841
+ var LOWER_HALF_BLOCK = "\u2584";
2842
+ var UPPER_HALF_BLOCK = "\u2580";
2843
+ function rgbString([r, g, b]) {
2844
+ return `rgb(${r},${g},${b})`;
2845
+ }
2846
+ function isBlack([r, g, b]) {
2847
+ return r === 0 && g === 0 && b === 0;
2848
+ }
2849
+ function Zorb({ size = 20 }) {
2850
+ if (!supportsTruecolor()) return null;
2851
+ const grid = generateZorbPixels(size);
2852
+ const rows = [];
2853
+ for (let y = 0; y < size; y += 2) {
2854
+ const topRow = grid[y];
2855
+ const bottomRow = y + 1 < size ? grid[y + 1] : void 0;
2856
+ const cells = [];
2857
+ for (let x = 0; x < size; x++) {
2858
+ const top = topRow[x];
2859
+ const bottom = bottomRow ? bottomRow[x] : [0, 0, 0];
2860
+ const topIsBlack = isBlack(top);
2861
+ const bottomIsBlack = isBlack(bottom);
2862
+ if (topIsBlack && bottomIsBlack) {
2863
+ cells.push(/* @__PURE__ */ jsx5(Text4, { children: " " }, x));
2864
+ } else if (topIsBlack) {
2865
+ cells.push(
2866
+ /* @__PURE__ */ jsx5(Text4, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
2867
+ );
2868
+ } else if (bottomIsBlack) {
2869
+ cells.push(
2870
+ /* @__PURE__ */ jsx5(Text4, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
2871
+ );
2872
+ } else {
2873
+ cells.push(
2874
+ /* @__PURE__ */ jsx5(
2875
+ Text4,
2876
+ {
2877
+ backgroundColor: rgbString(top),
2878
+ color: rgbString(bottom),
2879
+ children: LOWER_HALF_BLOCK
2880
+ },
2881
+ x
2882
+ )
2883
+ );
2884
+ }
2885
+ }
2886
+ rows.push(/* @__PURE__ */ jsx5(Text4, { children: cells }, y));
2887
+ }
2888
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2889
+ /* @__PURE__ */ jsx5(Text4, { children: " " }),
2890
+ rows,
2891
+ /* @__PURE__ */ jsx5(Text4, { children: " " })
2892
+ ] });
2893
+ }
2894
+
2895
+ // src/index.tsx
2896
+ import { jsx as jsx6 } from "react/jsx-runtime";
2897
+ if (process.env.ZORA_API_TARGET) {
2898
+ setApiBaseUrl(process.env.ZORA_API_TARGET);
2899
+ }
2900
+ var version = true ? "0.2.3" : JSON.parse(
2901
+ readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
2902
+ ).version;
2903
+ var buildProgram = () => {
2904
+ const program2 = new Command9().name("zora").description("Zora CLI").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
2905
+ program2.addCommand(authCommand);
2906
+ program2.addCommand(balanceCommand);
2907
+ program2.addCommand(buyCommand);
2908
+ program2.addCommand(exploreCommand);
2909
+ program2.addCommand(getCommand);
2910
+ program2.addCommand(setupCommand);
2911
+ program2.addCommand(walletCommand);
2912
+ program2.addCommand(sellCommand);
2913
+ return program2;
2914
+ };
2915
+ var program = buildProgram();
2916
+ if (!process.env.VITEST) {
2917
+ const showingHelp = process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h");
2918
+ if (showingHelp && !process.argv.includes("--json") && supportsTruecolor()) {
2919
+ renderOnce(/* @__PURE__ */ jsx6(Zorb, { size: 20 }));
2920
+ }
2921
+ identify();
2922
+ try {
2923
+ await program.parseAsync();
2924
+ } catch (err) {
2925
+ if (err instanceof ExitPromptError) {
2926
+ console.log("\nAborted.");
2927
+ process.exit(0);
2928
+ }
2929
+ throw err;
2930
+ } finally {
2931
+ await shutdownAnalytics();
2932
+ }
2933
+ }
2934
+ export {
2935
+ buildProgram
2936
+ };