@zoralabs/cli 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +76 -0
  2. package/dist/index.js +2569 -786
  3. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
- import { Command as Command9 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
  import { ExitPromptError } from "@inquirer/core";
6
6
  import "fs";
7
7
  import { setApiBaseUrl } from "@zoralabs/coins-sdk";
@@ -18,6 +18,50 @@ import {
18
18
  chmodSync
19
19
  } from "fs";
20
20
  import { join } from "path";
21
+
22
+ // src/lib/errors.ts
23
+ import { BaseError as ViemBaseError, InsufficientFundsError } from "viem";
24
+ function formatError(err) {
25
+ if (!(err instanceof Error)) return String(err);
26
+ const msg = err.message;
27
+ return msg.length > 120 ? msg.slice(0, 120) + "..." : msg;
28
+ }
29
+ function tradeErrorMessage(err) {
30
+ if (!(err instanceof Error)) return String(err);
31
+ if (err instanceof ViemBaseError) {
32
+ const insufficient = err.walk((e) => e instanceof InsufficientFundsError);
33
+ if (insufficient)
34
+ return "Not enough funds. Try a lower amount or run 'zora balance spendable' to check your balance.";
35
+ return err.shortMessage;
36
+ }
37
+ return apiErrorMessage(err);
38
+ }
39
+ function apiErrorMessage(err) {
40
+ if (!(err instanceof Error)) return String(err);
41
+ const code = err.code;
42
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND")
43
+ return "Can't connect. Check your internet connection.";
44
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT")
45
+ return "Request timed out. Try again.";
46
+ const status = err.status;
47
+ if (status === 429)
48
+ return "Rate limited. Wait a moment or run 'zora auth configure' for higher limits.";
49
+ if (status === 401 || status === 403)
50
+ return "Auth failed. Run 'zora auth configure' to update your API key.";
51
+ if (typeof status === "number" && status >= 500)
52
+ return "Zora is temporarily unavailable. Try again later.";
53
+ return formatError(err);
54
+ }
55
+ function fsErrorMessage(err, path) {
56
+ if (!(err instanceof Error)) return String(err);
57
+ const code = err.code;
58
+ if (code === "EACCES") return `Permission denied accessing ${path}.`;
59
+ if (code === "EISDIR")
60
+ return `Expected a file but found a directory at ${path}.`;
61
+ return formatError(err);
62
+ }
63
+
64
+ // src/lib/config.ts
21
65
  import { homedir, platform } from "os";
22
66
  function getConfigDir() {
23
67
  if (platform() === "win32") {
@@ -55,7 +99,7 @@ function readConfig() {
55
99
  parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
56
100
  } catch (err) {
57
101
  console.error(
58
- `Warning: could not parse ${CONFIG_FILE}: ${err.message}. Run 'zora auth configure' to fix.`
102
+ `Warning: could not parse ${CONFIG_FILE}: ${formatError(err)}. Run 'zora auth configure' to fix.`
59
103
  );
60
104
  configReadOnly = true;
61
105
  return { version: CONFIG_VERSION };
@@ -64,7 +108,7 @@ function readConfig() {
64
108
  assertVersion(parsed, CONFIG_VERSION, CONFIG_FILE);
65
109
  } catch (err) {
66
110
  console.error(
67
- `Warning: ${err.message}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
111
+ `Warning: ${formatError(err)}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
68
112
  );
69
113
  configReadOnly = true;
70
114
  return { version: CONFIG_VERSION };
@@ -155,7 +199,29 @@ function maskKey(key) {
155
199
  }
156
200
 
157
201
  // src/lib/output.ts
158
- var getJson = (cmd) => cmd.optsWithGlobals().json;
202
+ var getOutputMode = (cmd, defaultMode) => {
203
+ const globals = cmd.optsWithGlobals();
204
+ const json = globals.json ?? false;
205
+ const live = globals.live ?? false;
206
+ const static_ = globals.static ?? false;
207
+ const set = [
208
+ json && "--json",
209
+ live && "--live",
210
+ static_ && "--static"
211
+ ].filter(Boolean);
212
+ if (set.length > 1) {
213
+ return outputErrorAndExit(
214
+ false,
215
+ `${set.join(", ")} cannot be used together.`,
216
+ "Choose one: --json, --live, or --static"
217
+ );
218
+ }
219
+ if (json) return "json";
220
+ if (live) return "live";
221
+ if (static_) return "static";
222
+ return defaultMode;
223
+ };
224
+ var getJson = (cmd) => cmd.optsWithGlobals().json ?? false;
159
225
  var getYes = (cmd) => cmd.optsWithGlobals().yes ?? false;
160
226
  var outputJson = (data) => {
161
227
  console.log(JSON.stringify(data, null, 2));
@@ -177,8 +243,19 @@ var outputData = (json, opts) => {
177
243
  if (json) {
178
244
  outputJson(opts.json);
179
245
  } else {
180
- opts.table();
246
+ opts.render();
247
+ }
248
+ };
249
+ var getLiveConfig = (cmd, mode) => {
250
+ const live = mode === "live";
251
+ const intervalRaw = parseInt(cmd.opts().refresh, 10);
252
+ const intervalSeconds = isNaN(intervalRaw) || intervalRaw < 5 ? 30 : intervalRaw;
253
+ if (!live && cmd.getOptionValueSource("refresh") === "cli") {
254
+ console.warn(
255
+ "\x1B[33mWarning:\x1B[0m --refresh has no effect without --live"
256
+ );
181
257
  }
258
+ return { live, intervalSeconds };
182
259
  };
183
260
 
184
261
  // src/lib/prompt.ts
@@ -264,9 +341,7 @@ var resolveAccount = (json = false) => {
264
341
  try {
265
342
  return privateKeyToAccount(normalizeKey(key));
266
343
  } catch (err) {
267
- console.error(
268
- `\u2717 Invalid private key: ${err instanceof Error ? err.message : String(err)}`
269
- );
344
+ console.error(`\u2717 Invalid private key: ${formatError(err)}`);
270
345
  console.error(" Run 'zora setup --force' to replace it.");
271
346
  return process.exit(1);
272
347
  }
@@ -356,7 +431,7 @@ var getClient = () => {
356
431
  return client;
357
432
  };
358
433
  var commonProperties = () => ({
359
- cli_version: true ? "0.2.3" : "development",
434
+ cli_version: true ? "0.3.0" : "development",
360
435
  os: process.platform,
361
436
  arch: process.arch,
362
437
  node_version: process.version
@@ -429,7 +504,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
429
504
  status: "env_override",
430
505
  message: "API key is set via ZORA_API_KEY environment variable."
431
506
  },
432
- table: () => console.log(
507
+ render: () => console.log(
433
508
  "API key is set via ZORA_API_KEY environment variable. Unset it to configure manually."
434
509
  )
435
510
  });
@@ -457,7 +532,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
457
532
  saveApiKey(trimmed);
458
533
  outputData(json, {
459
534
  json: { saved: true, path: getConfigPath() },
460
- table: () => console.log(`API key saved to ${getConfigPath()}`)
535
+ render: () => console.log(`API key saved to ${getConfigPath()}`)
461
536
  });
462
537
  track("cli_auth_configure", {
463
538
  output_format: json ? "json" : "text"
@@ -465,7 +540,7 @@ authCommand.command("configure").description("Set your Zora API key").option("--
465
540
  } catch (err) {
466
541
  outputErrorAndExit(
467
542
  json,
468
- `Failed to save API key: ${err.message}`
543
+ `Failed to save API key: ${fsErrorMessage(err, getConfigPath())}`
469
544
  );
470
545
  }
471
546
  });
@@ -475,7 +550,7 @@ authCommand.command("status").description("Check authentication status").action(
475
550
  if (!apiKey) {
476
551
  outputData(json, {
477
552
  json: { authenticated: false },
478
- table: () => {
553
+ render: () => {
479
554
  console.log(
480
555
  "No API key configured. The CLI works without one, but requests are rate-limited."
481
556
  );
@@ -494,7 +569,7 @@ authCommand.command("status").description("Check authentication status").action(
494
569
  const source = getEnvApiKey() ? "env (ZORA_API_KEY)" : getConfigPath();
495
570
  outputData(json, {
496
571
  json: { authenticated: true, key: maskKey(apiKey), source },
497
- table: () => {
572
+ render: () => {
498
573
  console.log(`Authenticated: ${maskKey(apiKey)}`);
499
574
  console.log(`Source: ${source}`);
500
575
  }
@@ -506,86 +581,98 @@ authCommand.command("status").description("Check authentication status").action(
506
581
  });
507
582
  });
508
583
 
509
- // src/commands/balance.ts
510
- import { Command as Command3 } from "commander";
511
- import { getProfileBalances, setApiKey as setApiKey2 } from "@zoralabs/coins-sdk";
584
+ // src/commands/balance.tsx
585
+ import { Command as Command2 } from "commander";
586
+ import { Box as Box3, Text as Text3 } from "ink";
587
+ import { getProfileBalances, setApiKey } from "@zoralabs/coins-sdk";
512
588
 
513
589
  // src/lib/render.tsx
514
- import { renderToString } from "ink";
590
+ import { render, renderToString } from "ink";
515
591
  var renderOnce = (element) => {
516
- const output = renderToString(element);
592
+ const columns = process.stdout.columns || 80;
593
+ const output = renderToString(element, { columns });
517
594
  console.log(output);
518
595
  };
596
+ var renderLive = async (element) => {
597
+ const instance = render(element);
598
+ await instance.waitUntilExit();
599
+ };
600
+
601
+ // src/components/BalanceView.tsx
602
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef } from "react";
603
+ import { Box as Box2, Text as Text2, useInput, useApp } from "ink";
604
+ import Spinner from "ink-spinner";
519
605
 
520
606
  // src/components/table.tsx
521
607
  import { Box, Text } from "ink";
522
608
  import { jsx, jsxs } from "react/jsx-runtime";
609
+ var PADDING_LEFT = 1;
523
610
  var truncate = (str, max) => {
524
611
  if (str.length <= max) return str;
525
612
  return str.slice(0, max - 1) + "\u2026";
526
613
  };
527
- var TableComponent = ({
614
+ var computeColumnWidths = (columns, fullWidth) => {
615
+ const baseWidths = columns.map((col) => col.width);
616
+ const totalBase = baseWidths.reduce((sum, w) => sum + w, 0);
617
+ if (!fullWidth) return baseWidths;
618
+ const width = process.stdout.columns ?? 80;
619
+ const available = width - PADDING_LEFT;
620
+ if (available <= totalBase) return baseWidths;
621
+ const computed = baseWidths.map(
622
+ (w) => Math.max(1, Math.floor(w / totalBase * available))
623
+ );
624
+ let remainder = available - computed.reduce((sum, w) => sum + w, 0);
625
+ for (let i = 0; i < computed.length && remainder > 0; i++) {
626
+ computed[i]++;
627
+ remainder--;
628
+ }
629
+ return computed;
630
+ };
631
+ var Table = ({
528
632
  columns,
529
633
  data,
530
634
  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";
635
+ subtitle,
636
+ fullWidth = true,
637
+ footer,
638
+ selectedRow
639
+ }) => {
640
+ const widths = computeColumnWidths(columns, fullWidth);
641
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [
642
+ title && /* @__PURE__ */ jsxs(Box, { paddingLeft: PADDING_LEFT, marginBottom: 1, children: [
643
+ /* @__PURE__ */ jsx(Text, { bold: true, children: title }),
644
+ subtitle && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
645
+ " ",
646
+ subtitle
647
+ ] })
648
+ ] }),
649
+ /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, i) => /* @__PURE__ */ jsx(Box, { width: widths[i], children: /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, wrap: "truncate", children: col.header }) }, col.header)) }),
650
+ data.map((row, i) => {
651
+ const isSelected = selectedRow === i;
652
+ return /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, colIdx) => {
653
+ const colWidth = widths[colIdx];
654
+ const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), colWidth - 2);
655
+ const colorName = col.color?.(row);
656
+ return /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(
657
+ Text,
658
+ {
659
+ color: colorName,
660
+ bold: isSelected,
661
+ inverse: isSelected,
662
+ wrap: col.noTruncate ? "wrap" : "truncate",
663
+ children: value
664
+ }
665
+ ) }, col.header);
666
+ }) }, i);
667
+ }),
668
+ footer && /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: footer }) })
669
+ ] });
670
+ };
574
671
 
575
672
  // src/lib/format.ts
576
673
  import { format, formatDistanceStrict } from "date-fns";
577
674
  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) {
675
+ function formatCompactUsd(value) {
589
676
  if (!value || Number(value) === 0) return "$0";
590
677
  return new Intl.NumberFormat("en-US", {
591
678
  style: "currency",
@@ -640,247 +727,14 @@ var formatEthDisplay = (wei) => {
640
727
  const trimmed = parts[1].replace(/0+$/, "") || "0";
641
728
  return `${parts[0]}.${trimmed}`;
642
729
  };
730
+ var truncateAddress = (address) => `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
643
731
  var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
644
732
  maximumFractionDigits: 2
645
733
  }).format(Number(coinsOut));
646
734
 
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
735
  // src/lib/balance-format.ts
882
736
  var COIN_DECIMALS = 18;
883
- var toHumanBalance = (rawBalance) => Number(normalizeTokenAmount(rawBalance));
737
+ var parseRawBalance = (rawBalance) => Number(normalizeTokenAmount(rawBalance));
884
738
  var normalizeTokenAmount = (rawBalance, decimals = COIN_DECIMALS) => {
885
739
  try {
886
740
  const value = BigInt(rawBalance);
@@ -895,20 +749,20 @@ var normalizeTokenAmount = (rawBalance, decimals = COIN_DECIMALS) => {
895
749
  return rawBalance;
896
750
  }
897
751
  };
898
- var formatUsdValue = (balance, priceInUsdc) => {
752
+ var formatBalanceAsUsd = (balance, priceInUsdc) => {
899
753
  if (!priceInUsdc) return "-";
900
- const value = toHumanBalance(balance) * Number(priceInUsdc);
754
+ const value = parseRawBalance(balance) * Number(priceInUsdc);
901
755
  if (value < 0.01) return "<$0.01";
902
756
  return formatUsd(value);
903
757
  };
904
758
  var formatBalance = (balance) => {
905
- const n = toHumanBalance(balance);
759
+ const n = parseRawBalance(balance);
906
760
  if (n === 0) return "0";
907
761
  if (n < 1e-3) return "<0.001";
908
762
  if (n < 1) return n.toFixed(4);
909
763
  return new Intl.NumberFormat("en-US", {
910
764
  notation: "compact",
911
- compactDisplay: "long",
765
+ compactDisplay: "short",
912
766
  maximumFractionDigits: 1
913
767
  }).format(n);
914
768
  };
@@ -918,27 +772,267 @@ var trimTrailingZeros = (value) => {
918
772
  return trimmed || "0";
919
773
  };
920
774
 
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 = [
775
+ // src/lib/balance-columns.tsx
776
+ var SORT_LABELS = {
777
+ "usd-value": "USD Value",
778
+ balance: "Balance",
779
+ "market-cap": "Market Cap",
780
+ "price-change": "Price Change"
781
+ };
782
+ var walletColumns = [
783
+ { header: "Name", width: 14, accessor: (row) => row.name },
931
784
  {
932
- name: "Ether",
933
- symbol: "ETH",
934
- address: WETH_ADDRESS,
935
- decimals: 18,
936
- priceAddress: WETH_ADDRESS,
937
- isNative: true
785
+ header: "Symbol",
786
+ width: 10,
787
+ noTruncate: true,
788
+ accessor: (row) => row.symbol
938
789
  },
939
- {
940
- name: "USD Coin",
941
- symbol: "USDC",
790
+ { header: "Balance", width: 20, accessor: (row) => row.balance },
791
+ { header: "USD Value", width: 16, accessor: (row) => row.usdValue }
792
+ ];
793
+ var balanceColumns = [
794
+ { header: "#", width: 5, accessor: (row) => String(row.rank) },
795
+ {
796
+ header: "Name",
797
+ width: 24,
798
+ accessor: (row) => row.coin?.name ?? "Unknown"
799
+ },
800
+ {
801
+ header: "Symbol",
802
+ width: 12,
803
+ noTruncate: true,
804
+ accessor: (row) => row.coin?.symbol ?? ""
805
+ },
806
+ {
807
+ header: "Balance",
808
+ width: 14,
809
+ accessor: (row) => formatBalance(row.balance)
810
+ },
811
+ {
812
+ header: "USD Value",
813
+ width: 14,
814
+ accessor: (row) => formatBalanceAsUsd(row.balance, row.coin?.tokenPrice?.priceInUsdc)
815
+ },
816
+ {
817
+ header: "Market Cap",
818
+ width: 14,
819
+ accessor: (row) => formatCompactUsd(row.coin?.marketCap)
820
+ },
821
+ {
822
+ header: "24h Change",
823
+ width: 12,
824
+ accessor: (row) => formatMcapChange(row.coin?.marketCap, row.coin?.marketCapDelta24h).text,
825
+ color: (row) => formatMcapChange(row.coin?.marketCap, row.coin?.marketCapDelta24h).color
826
+ }
827
+ ];
828
+
829
+ // src/hooks/use-auto-refresh.ts
830
+ import { useState, useEffect, useCallback } from "react";
831
+ var useAutoRefresh = (intervalSeconds, enabled) => {
832
+ const [refreshCount, setRefreshCount] = useState(0);
833
+ const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(intervalSeconds);
834
+ const [resetCount, setResetCount] = useState(0);
835
+ const triggerManualRefresh = useCallback(() => {
836
+ if (!enabled) return;
837
+ setRefreshCount((c) => c + 1);
838
+ setSecondsUntilRefresh(intervalSeconds);
839
+ setResetCount((c) => c + 1);
840
+ }, [enabled, intervalSeconds]);
841
+ useEffect(() => {
842
+ if (!enabled) return;
843
+ setSecondsUntilRefresh(intervalSeconds);
844
+ const ticker = setInterval(() => {
845
+ setSecondsUntilRefresh((prev) => {
846
+ if (prev <= 1) {
847
+ setRefreshCount((c) => c + 1);
848
+ return intervalSeconds;
849
+ }
850
+ return prev - 1;
851
+ });
852
+ }, 1e3);
853
+ return () => clearInterval(ticker);
854
+ }, [enabled, intervalSeconds, resetCount]);
855
+ if (!enabled) {
856
+ return { refreshCount: 0, secondsUntilRefresh: 0, triggerManualRefresh };
857
+ }
858
+ return { refreshCount, secondsUntilRefresh, triggerManualRefresh };
859
+ };
860
+
861
+ // src/components/BalanceView.tsx
862
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
863
+ var BalanceView = ({
864
+ fetchData,
865
+ sort,
866
+ mode = "full",
867
+ initialCursor,
868
+ autoRefresh = false,
869
+ intervalSeconds = 30
870
+ }) => {
871
+ const { exit } = useApp();
872
+ const [loading, setLoading] = useState2(true);
873
+ const [isRefreshing, setIsRefreshing] = useState2(false);
874
+ const [error, setError] = useState2(null);
875
+ const [data, setData] = useState2(null);
876
+ const paginated = mode === "coins";
877
+ const [page, setPage] = useState2(1);
878
+ const [cursorHistory, setCursorHistory] = useState2(
879
+ []
880
+ );
881
+ const [currentCursor, setCurrentCursor] = useState2(
882
+ initialCursor
883
+ );
884
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
885
+ const [manualRefreshCount, setManualRefreshCount] = useState2(0);
886
+ const hasLoadedOnce = useRef(false);
887
+ const load = useCallback2(
888
+ async (cursor) => {
889
+ if (hasLoadedOnce.current) {
890
+ setIsRefreshing(true);
891
+ } else {
892
+ setLoading(true);
893
+ }
894
+ setError(null);
895
+ try {
896
+ const result = await fetchData(cursor);
897
+ setData(result);
898
+ hasLoadedOnce.current = true;
899
+ } catch (err) {
900
+ setError(err instanceof Error ? err.message : String(err));
901
+ }
902
+ setLoading(false);
903
+ setIsRefreshing(false);
904
+ },
905
+ [fetchData]
906
+ );
907
+ useEffect2(() => {
908
+ load(currentCursor);
909
+ }, [load, refreshCount, manualRefreshCount, currentCursor]);
910
+ useInput((input, key) => {
911
+ if (input === "q" || key.escape) {
912
+ exit();
913
+ return;
914
+ }
915
+ if (loading) return;
916
+ if (input === "r") {
917
+ triggerManualRefresh();
918
+ setManualRefreshCount((c) => c + 1);
919
+ return;
920
+ }
921
+ if (!paginated) return;
922
+ const canGoNext = data?.pageInfo?.hasNextPage && data.pageInfo.endCursor;
923
+ const canGoPrev = cursorHistory.length > 0;
924
+ if ((input === "n" || key.rightArrow) && canGoNext) {
925
+ setCursorHistory((prev) => [...prev, currentCursor]);
926
+ setCurrentCursor(data.pageInfo.endCursor);
927
+ setPage((p) => p + 1);
928
+ }
929
+ if ((input === "p" || key.leftArrow) && canGoPrev) {
930
+ const prev = cursorHistory[cursorHistory.length - 1];
931
+ setCursorHistory((h) => h.slice(0, -1));
932
+ setCurrentCursor(prev);
933
+ setPage((p) => p - 1);
934
+ }
935
+ });
936
+ if (error && !data) {
937
+ return /* @__PURE__ */ jsxs2(
938
+ Box2,
939
+ {
940
+ flexDirection: "column",
941
+ paddingLeft: 1,
942
+ paddingTop: 1,
943
+ paddingBottom: 1,
944
+ children: [
945
+ /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
946
+ "Error: ",
947
+ error
948
+ ] }),
949
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Press q to exit" }) })
950
+ ]
951
+ }
952
+ );
953
+ }
954
+ if (loading && !data) {
955
+ return /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
956
+ /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
957
+ " Loading\u2026"
958
+ ] }) });
959
+ }
960
+ if (!data) return null;
961
+ const hints = [];
962
+ if (paginated && cursorHistory.length > 0) hints.push("\u2190 prev");
963
+ if (paginated && data?.pageInfo?.hasNextPage) hints.push("\u2192 next");
964
+ hints.push("r refresh");
965
+ if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
966
+ hints.push("q quit");
967
+ const footer = hints.join(" \xB7 ");
968
+ const showWallet = mode === "full" || mode === "wallet";
969
+ const showCoins = mode === "full" || mode === "coins";
970
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
971
+ isRefreshing && /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
972
+ /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
973
+ " Refreshing\u2026"
974
+ ] }) }),
975
+ showWallet && /* @__PURE__ */ jsx2(
976
+ Table,
977
+ {
978
+ columns: walletColumns,
979
+ data: data.walletBalances,
980
+ title: "Wallet"
981
+ }
982
+ ),
983
+ showCoins && data.rankedBalances.length === 0 ? /* @__PURE__ */ jsxs2(
984
+ Box2,
985
+ {
986
+ flexDirection: "column",
987
+ paddingLeft: 1,
988
+ paddingTop: 1,
989
+ paddingBottom: 1,
990
+ children: [
991
+ /* @__PURE__ */ jsx2(Text2, { children: "No coin balances found." }),
992
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
993
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Buy coins to see them here:" }),
994
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
995
+ " zora buy ",
996
+ "<address>",
997
+ " --eth 0.001"
998
+ ] })
999
+ ] })
1000
+ ]
1001
+ }
1002
+ ) : showCoins ? /* @__PURE__ */ jsx2(
1003
+ Table,
1004
+ {
1005
+ columns: balanceColumns,
1006
+ data: data.rankedBalances,
1007
+ title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1008
+ subtitle: paginated ? `Page ${page} \xB7 ${data.rankedBalances.length} result${data.rankedBalances.length !== 1 ? "s" : ""}` : `${data.rankedBalances.length} of ${data.total}`
1009
+ }
1010
+ ) : null,
1011
+ /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: footer }) })
1012
+ ] });
1013
+ };
1014
+
1015
+ // src/lib/wallet-balances.ts
1016
+ import { getTokenInfo } from "@zoralabs/coins-sdk";
1017
+ import {
1018
+ createPublicClient as createPublicClient2,
1019
+ erc20Abi,
1020
+ formatUnits,
1021
+ http
1022
+ } from "viem";
1023
+ import { base as base2 } from "viem/chains";
1024
+ var TRACKED_TOKENS = [
1025
+ {
1026
+ name: "Ether",
1027
+ symbol: "ETH",
1028
+ address: WETH_ADDRESS,
1029
+ decimals: 18,
1030
+ priceAddress: WETH_ADDRESS,
1031
+ isNative: true
1032
+ },
1033
+ {
1034
+ name: "USD Coin",
1035
+ symbol: "USDC",
942
1036
  address: USDC_ADDRESS,
943
1037
  decimals: USDC_DECIMALS,
944
1038
  priceAddress: USDC_ADDRESS,
@@ -958,7 +1052,7 @@ var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
958
1052
  return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
959
1053
  } catch (err) {
960
1054
  console.warn(
961
- `Warning: failed to fetch price for ${address}: ${err instanceof Error ? err.message : String(err)}`
1055
+ `Warning: failed to fetch price for ${address}: ${formatError(err)}`
962
1056
  );
963
1057
  return null;
964
1058
  }
@@ -1033,78 +1127,21 @@ var fetchWalletBalances = async (walletAddress) => {
1033
1127
  return { walletBalances, walletBalancesJson };
1034
1128
  };
1035
1129
 
1036
- // src/commands/balance.ts
1130
+ // src/commands/balance.tsx
1131
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1037
1132
  var SORT_MAP = {
1038
1133
  "usd-value": "USD_VALUE",
1039
1134
  balance: "BALANCE",
1040
1135
  "market-cap": "MARKET_CAP",
1041
1136
  "price-change": "PRICE_CHANGE"
1042
1137
  };
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(", ");
1138
+ var SORT_OPTIONS = Object.keys(SORT_LABELS).join(", ");
1050
1139
  var extractErrorMessage = (error) => {
1051
1140
  if (typeof error === "object" && error !== null && "error" in error) {
1052
1141
  return String(error.error);
1053
1142
  }
1054
1143
  return JSON.stringify(error);
1055
1144
  };
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
1145
  var formatBalanceJson = (balance, rank) => {
1109
1146
  const priceUsd = balance.coin?.tokenPrice?.priceInUsdc;
1110
1147
  const marketCap = balance.coin?.marketCap ? Number(balance.coin.marketCap) : null;
@@ -1112,7 +1149,7 @@ var formatBalanceJson = (balance, rank) => {
1112
1149
  const volume24h = balance.coin?.volume24h ? Number(balance.coin.volume24h) : null;
1113
1150
  const totalVolume = balance.coin?.totalVolume ? Number(balance.coin.totalVolume) : null;
1114
1151
  const priceUsdValue = priceUsd ? Number(priceUsd) : null;
1115
- const usdValue = priceUsdValue !== null ? Number((toHumanBalance(balance.balance) * priceUsdValue).toFixed(6)) : null;
1152
+ const usdValue = priceUsdValue !== null ? Number((parseRawBalance(balance.balance) * priceUsdValue).toFixed(6)) : null;
1116
1153
  const marketCapChange24h = marketCap !== null && marketCapDelta24h !== null && marketCap - marketCapDelta24h !== 0 ? Number(
1117
1154
  (marketCapDelta24h / (marketCap - marketCapDelta24h) * 100).toFixed(
1118
1155
  4
@@ -1146,24 +1183,27 @@ function resolveContext(json) {
1146
1183
  "Not authenticated. Run 'zora auth configure' to set your API key."
1147
1184
  );
1148
1185
  }
1149
- setApiKey2(apiKey);
1186
+ setApiKey(apiKey);
1150
1187
  return account;
1151
1188
  }
1152
1189
  function renderWallet(json, walletResult) {
1153
1190
  outputData(json, {
1154
1191
  json: { wallet: walletResult.walletBalancesJson },
1155
- table: () => {
1192
+ render: () => {
1156
1193
  renderOnce(
1157
- TableComponent({
1158
- columns: walletColumns,
1159
- data: walletResult.walletBalances,
1160
- title: "Wallet"
1161
- })
1194
+ /* @__PURE__ */ jsx3(
1195
+ Table,
1196
+ {
1197
+ columns: walletColumns,
1198
+ data: walletResult.walletBalances,
1199
+ title: "Wallet"
1200
+ }
1201
+ )
1162
1202
  );
1163
1203
  }
1164
1204
  });
1165
1205
  }
1166
- function renderCoins(json, balances, total, sort) {
1206
+ function renderCoins(json, balances, total, sort, limit, pageInfo) {
1167
1207
  const rankedBalances = balances.map((balance, index) => ({
1168
1208
  ...balance,
1169
1209
  rank: index + 1
@@ -1172,39 +1212,43 @@ function renderCoins(json, balances, total, sort) {
1172
1212
  json: {
1173
1213
  coins: rankedBalances.map(
1174
1214
  (balance) => formatBalanceJson(balance, balance.rank)
1175
- )
1215
+ ),
1216
+ pageInfo: pageInfo ?? null
1176
1217
  },
1177
- table: () => {
1218
+ render: () => {
1178
1219
  if (balances.length === 0) {
1179
1220
  console.log("\n No coin balances found.\n");
1180
1221
  console.log(" Buy coins to see them here:");
1181
1222
  console.log(" zora buy <address> --eth 0.001\n");
1182
1223
  } else {
1224
+ const footer = pageInfo?.hasNextPage && pageInfo.endCursor ? `Next page: zora balance coins --sort ${sort} --limit ${limit} --after ${pageInfo.endCursor}` : void 0;
1183
1225
  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
- })
1226
+ /* @__PURE__ */ jsx3(
1227
+ Table,
1228
+ {
1229
+ columns: balanceColumns,
1230
+ data: rankedBalances,
1231
+ title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1232
+ subtitle: `${balances.length} of ${total}`,
1233
+ footer
1234
+ }
1235
+ )
1190
1236
  );
1191
1237
  }
1192
1238
  }
1193
1239
  });
1194
1240
  }
1195
- async function fetchCoins(json, address, sort, limit) {
1241
+ async function fetchCoins(json, address, sort, limit, after) {
1196
1242
  let response;
1197
1243
  try {
1198
1244
  response = await getProfileBalances({
1199
1245
  identifier: address,
1200
1246
  count: limit,
1201
- sortOption: SORT_MAP[sort]
1247
+ sortOption: SORT_MAP[sort],
1248
+ after
1202
1249
  });
1203
1250
  } catch (err) {
1204
- outputErrorAndExit(
1205
- json,
1206
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1207
- );
1251
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
1208
1252
  }
1209
1253
  if (response.error) {
1210
1254
  outputErrorAndExit(
@@ -1217,14 +1261,15 @@ async function fetchCoins(json, address, sort, limit) {
1217
1261
  (e) => e.node
1218
1262
  );
1219
1263
  const total = response.data?.profile?.coinBalances?.count ?? balances.length;
1220
- return { balances, total };
1264
+ const pageInfo = response.data?.profile?.coinBalances?.pageInfo;
1265
+ return { balances, total, pageInfo };
1221
1266
  }
1222
1267
  function validateCoinOpts(json, sort, limitStr) {
1223
1268
  if (!SORT_MAP[sort]) {
1224
1269
  outputErrorAndExit(
1225
1270
  json,
1226
1271
  `Invalid --sort value: ${sort}.`,
1227
- `Supported: ${SORT_OPTIONS2}`
1272
+ `Supported: ${SORT_OPTIONS}`
1228
1273
  );
1229
1274
  }
1230
1275
  const limit = parseInt(limitStr, 10);
@@ -1236,100 +1281,255 @@ function validateCoinOpts(json, sort, limitStr) {
1236
1281
  }
1237
1282
  return { sort, limit };
1238
1283
  }
1239
- var balanceCommand = new Command3("balance").description("Show balances in your wallet").action(async function() {
1240
- const json = getJson(this);
1284
+ var balanceCommand = new Command2("balance").description("Show balances in your wallet").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1285
+ "--refresh <seconds>",
1286
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1287
+ "30"
1288
+ ).action(async function() {
1289
+ const output = getOutputMode(this, "live");
1290
+ const json = output === "json";
1241
1291
  const account = resolveContext(json);
1292
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1242
1293
  const sort = "usd-value";
1243
1294
  const limit = 10;
1244
- let walletResult;
1245
- let coinsResult;
1246
- try {
1247
- [walletResult, coinsResult] = await Promise.all([
1295
+ const fetchBalanceData = async () => {
1296
+ const [walletResult, coinsResult] = await Promise.allSettled([
1248
1297
  fetchWalletBalances(account.address),
1249
1298
  fetchCoins(json, account.address, sort, limit)
1250
1299
  ]);
1251
- } catch (err) {
1252
- outputErrorAndExit(
1253
- json,
1254
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
1300
+ if (walletResult.status === "rejected" || coinsResult.status === "rejected") {
1301
+ const err = walletResult.status === "rejected" ? walletResult.reason : coinsResult.reason;
1302
+ throw err instanceof Error ? err : new Error(String(err));
1303
+ }
1304
+ const rankedBalances = coinsResult.value.balances.map(
1305
+ (balance, index) => ({
1306
+ ...balance,
1307
+ rank: index + 1
1308
+ })
1255
1309
  );
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)
1310
+ return {
1311
+ walletBalances: walletResult.value.walletBalances,
1312
+ walletBalancesJson: walletResult.value.walletBalancesJson,
1313
+ rankedBalances,
1314
+ total: coinsResult.value.total
1315
+ };
1316
+ };
1317
+ if (json) {
1318
+ const data = await fetchBalanceData().catch(
1319
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1320
+ );
1321
+ outputData(json, {
1322
+ json: {
1323
+ wallet: data.walletBalancesJson,
1324
+ coins: data.rankedBalances.map(
1325
+ (balance) => formatBalanceJson(balance, balance.rank)
1326
+ )
1327
+ },
1328
+ render: () => {
1329
+ }
1330
+ });
1331
+ track("cli_balances", {
1332
+ sort,
1333
+ limit,
1334
+ live: false,
1335
+ result_count: data.rankedBalances.length,
1336
+ total_count: data.total,
1337
+ output_format: "json"
1338
+ });
1339
+ } else if (live) {
1340
+ await renderLive(
1341
+ /* @__PURE__ */ jsx3(
1342
+ BalanceView,
1343
+ {
1344
+ fetchData: fetchBalanceData,
1345
+ sort,
1346
+ mode: "full",
1347
+ autoRefresh: live,
1348
+ intervalSeconds
1349
+ }
1266
1350
  )
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({
1351
+ );
1352
+ track("cli_balances", {
1353
+ sort,
1354
+ limit,
1355
+ live,
1356
+ interval: intervalSeconds,
1357
+ output_format: "live"
1358
+ });
1359
+ } else {
1360
+ const data = await fetchBalanceData().catch(
1361
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1362
+ );
1363
+ renderOnce(
1364
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1365
+ /* @__PURE__ */ jsx3(
1366
+ Table,
1367
+ {
1368
+ columns: walletColumns,
1369
+ data: data.walletBalances,
1370
+ title: "Wallet"
1371
+ }
1372
+ ),
1373
+ data.rankedBalances.length === 0 ? /* @__PURE__ */ jsxs3(
1374
+ Box3,
1375
+ {
1376
+ flexDirection: "column",
1377
+ paddingLeft: 1,
1378
+ paddingTop: 1,
1379
+ paddingBottom: 1,
1380
+ children: [
1381
+ /* @__PURE__ */ jsx3(Text3, { children: "No coin balances found." }),
1382
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1383
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Buy coins to see them here:" }),
1384
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1385
+ " zora buy ",
1386
+ "<address>",
1387
+ " --eth 0.001"
1388
+ ] })
1389
+ ] })
1390
+ ]
1391
+ }
1392
+ ) : /* @__PURE__ */ jsx3(
1393
+ Table,
1394
+ {
1283
1395
  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
- });
1396
+ data: data.rankedBalances,
1397
+ title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1398
+ subtitle: `${data.rankedBalances.length} of ${data.total}`
1399
+ }
1400
+ )
1401
+ ] })
1402
+ );
1403
+ track("cli_balances", {
1404
+ sort,
1405
+ limit,
1406
+ live: false,
1407
+ result_count: data.rankedBalances.length,
1408
+ total_count: data.total,
1409
+ output_format: "static"
1410
+ });
1411
+ }
1299
1412
  });
1300
- balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").action(async function() {
1301
- const json = getJson(this);
1413
+ balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1414
+ "--refresh <seconds>",
1415
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1416
+ "30"
1417
+ ).action(async function() {
1418
+ const output = getOutputMode(this, "live");
1419
+ const json = output === "json";
1302
1420
  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)}`
1421
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1422
+ const fetchSpendableData = async () => {
1423
+ const walletResult = await fetchWalletBalances(account.address);
1424
+ return {
1425
+ walletBalances: walletResult.walletBalances,
1426
+ walletBalancesJson: walletResult.walletBalancesJson,
1427
+ rankedBalances: [],
1428
+ total: 0
1429
+ };
1430
+ };
1431
+ if (json) {
1432
+ const data = await fetchSpendableData().catch(
1433
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1434
+ );
1435
+ outputData(json, {
1436
+ json: { wallet: data.walletBalancesJson },
1437
+ render: () => {
1438
+ }
1439
+ });
1440
+ } else if (live) {
1441
+ await renderLive(
1442
+ /* @__PURE__ */ jsx3(
1443
+ BalanceView,
1444
+ {
1445
+ fetchData: fetchSpendableData,
1446
+ sort: "usd-value",
1447
+ mode: "wallet",
1448
+ autoRefresh: live,
1449
+ intervalSeconds
1450
+ }
1451
+ )
1452
+ );
1453
+ } else {
1454
+ const walletResult = await fetchWalletBalances(account.address).catch(
1455
+ (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1310
1456
  );
1457
+ renderWallet(json, walletResult);
1311
1458
  }
1312
- renderWallet(json, walletResult);
1313
1459
  });
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);
1460
+ balanceCommand.command("coins").description("Show coin positions").option("--sort <sort>", `Sort by: ${SORT_OPTIONS}`, "usd-value").option("--limit <n>", "Number of results (max 20)", "10").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
1461
+ "--refresh <seconds>",
1462
+ "Auto-refresh interval in seconds, requires --live (min 5)",
1463
+ "30"
1464
+ ).option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
1465
+ const output = getOutputMode(this, "live");
1466
+ const json = output === "json";
1316
1467
  const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
1468
+ const after = opts.after;
1317
1469
  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);
1470
+ const { live, intervalSeconds } = getLiveConfig(this, output);
1471
+ const fetchCoinsPage = async (cursor) => {
1472
+ const { balances, total, pageInfo } = await fetchCoins(
1473
+ json,
1474
+ account.address,
1475
+ sort,
1476
+ limit,
1477
+ cursor
1478
+ );
1479
+ const rankedBalances = balances.map((balance, index) => ({
1480
+ ...balance,
1481
+ rank: index + 1
1482
+ }));
1483
+ return {
1484
+ walletBalances: [],
1485
+ walletBalancesJson: [],
1486
+ rankedBalances,
1487
+ total,
1488
+ pageInfo
1489
+ };
1490
+ };
1491
+ if (json) {
1492
+ const data = await fetchCoinsPage(after);
1493
+ renderCoins(
1494
+ json,
1495
+ data.rankedBalances,
1496
+ data.total,
1497
+ sort,
1498
+ limit,
1499
+ data.pageInfo
1500
+ );
1501
+ } else if (live) {
1502
+ await renderLive(
1503
+ /* @__PURE__ */ jsx3(
1504
+ BalanceView,
1505
+ {
1506
+ fetchData: fetchCoinsPage,
1507
+ sort,
1508
+ mode: "coins",
1509
+ initialCursor: after,
1510
+ autoRefresh: live,
1511
+ intervalSeconds
1512
+ }
1513
+ )
1514
+ );
1515
+ } else {
1516
+ const { balances, total, pageInfo } = await fetchCoins(
1517
+ json,
1518
+ account.address,
1519
+ sort,
1520
+ limit,
1521
+ after
1522
+ );
1523
+ renderCoins(json, balances, total, sort, limit, pageInfo);
1524
+ }
1325
1525
  });
1326
1526
 
1327
1527
  // src/commands/buy.ts
1328
- import { Command as Command4 } from "commander";
1528
+ import { Command as Command3 } from "commander";
1329
1529
  import confirm2 from "@inquirer/confirm";
1330
1530
  import { parseUnits, formatUnits as formatUnits3, isAddress } from "viem";
1331
1531
  import {
1332
- setApiKey as setApiKey3,
1532
+ setApiKey as setApiKey2,
1333
1533
  getCoin,
1334
1534
  tradeCoin,
1335
1535
  createTradeCall
@@ -1343,7 +1543,7 @@ import {
1343
1543
  parseEventLogs,
1344
1544
  erc20Abi as erc20Abi2
1345
1545
  } from "viem";
1346
- var GAS_RESERVE = parseEther("0.001");
1546
+ var GAS_RESERVE = parseEther("0.00001");
1347
1547
  var BUY_AMOUNT_CHECKS = {
1348
1548
  eth: (opts) => opts.eth !== void 0,
1349
1549
  usd: (opts) => opts.usd !== void 0,
@@ -1409,13 +1609,16 @@ var getReceivedAmountFromReceipt = ({
1409
1609
  if (matchingTransfers.length === 0) {
1410
1610
  throw new Error("No matching Transfer event found in receipt.");
1411
1611
  }
1412
- return matchingTransfers.reduce((total, transfer) => {
1612
+ const amount = matchingTransfers.reduce((total, transfer) => {
1413
1613
  const value = transfer.args?.value;
1414
1614
  if (value === void 0) {
1415
1615
  throw new Error("Transfer event missing amount.");
1416
1616
  }
1417
1617
  return total + value;
1418
1618
  }, 0n);
1619
+ const lastTransfer = matchingTransfers[matchingTransfers.length - 1];
1620
+ const logIndex = lastTransfer?.logIndex ?? null;
1621
+ return { amount, logIndex };
1419
1622
  };
1420
1623
  var printDebugRequest = (label, tradeParameters) => {
1421
1624
  if (process.env.ZORA_API_TARGET) {
@@ -1505,19 +1708,12 @@ var printTradeResult = (json, info) => {
1505
1708
  };
1506
1709
 
1507
1710
  // 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";
1711
+ var buyCommand = new Command3("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").action(async function(coinAddress, opts) {
1712
+ const json = getJson(this);
1510
1713
  const debug = opts.debug === true;
1511
1714
  if (!isAddress(coinAddress)) {
1512
1715
  outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
1513
1716
  }
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
1717
  const tokenKey = opts.token.toLowerCase();
1522
1718
  if (!(tokenKey in BASE_TRADE_TOKENS)) {
1523
1719
  outputErrorAndExit(
@@ -1542,7 +1738,7 @@ var buyCommand = new Command4("buy").description("Buy a coin").argument("<addres
1542
1738
  const slippage = slippagePct / 100;
1543
1739
  const apiKey = getApiKey();
1544
1740
  if (apiKey) {
1545
- setApiKey3(apiKey);
1741
+ setApiKey2(apiKey);
1546
1742
  }
1547
1743
  const account = resolveAccount(json);
1548
1744
  const { publicClient, walletClient } = createClients(account);
@@ -1551,10 +1747,7 @@ var buyCommand = new Command4("buy").description("Buy a coin").argument("<addres
1551
1747
  const response = await getCoin({ address: coinAddress });
1552
1748
  token = response.data?.zora20Token;
1553
1749
  } catch (err) {
1554
- outputErrorAndExit(
1555
- json,
1556
- `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
1557
- );
1750
+ outputErrorAndExit(json, `Failed to fetch coin: ${apiErrorMessage(err)}`);
1558
1751
  }
1559
1752
  if (!token) {
1560
1753
  outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
@@ -1648,7 +1841,7 @@ var buyCommand = new Command4("buy").description("Buy a coin").argument("<addres
1648
1841
  if (isEth && balance <= gasReserve) {
1649
1842
  outputErrorAndExit(
1650
1843
  json,
1651
- `Balance too low (${formatEthDisplay(balance)} ETH). Need >0.001 ETH for gas.`
1844
+ `Balance too low (${formatEthDisplay(balance)} ETH). Need >${formatEthDisplay(GAS_RESERVE)} ETH for gas.`
1652
1845
  );
1653
1846
  }
1654
1847
  const spendableBalance = balance - gasReserve;
@@ -1662,7 +1855,7 @@ var buyCommand = new Command4("buy").description("Buy a coin").argument("<addres
1662
1855
  "Invalid --percent value. Must be between 0 and 100."
1663
1856
  );
1664
1857
  }
1665
- amountIn = pct === 100 ? spendableBalance : balance * BigInt(Math.round(pct * 100)) / 10000n;
1858
+ amountIn = pct === 100 ? spendableBalance : spendableBalance * BigInt(Math.round(pct * 100)) / 10000n;
1666
1859
  if (amountIn === 0n) {
1667
1860
  outputErrorAndExit(
1668
1861
  json,
@@ -1671,6 +1864,17 @@ var buyCommand = new Command4("buy").description("Buy a coin").argument("<addres
1671
1864
  }
1672
1865
  }
1673
1866
  }
1867
+ let swapAmountUsd;
1868
+ if (amountMode === "usd") {
1869
+ swapAmountUsd = parsePercentageLikeValue(opts.usd);
1870
+ } else {
1871
+ const priceUsd = inputToken.fixedPriceUsd ?? await fetchTokenPriceUsd(inputToken.priceAddress);
1872
+ if (priceUsd != null) {
1873
+ swapAmountUsd = Number(
1874
+ (Number(formatUnits3(amountIn, inputToken.decimals)) * priceUsd).toFixed(2)
1875
+ );
1876
+ }
1877
+ }
1674
1878
  const tradeParameters = {
1675
1879
  sell: inputToken.trade,
1676
1880
  buy: { type: "erc20", address: coinAddress },
@@ -1718,7 +1922,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1718
1922
  }
1719
1923
  outputErrorAndExit(
1720
1924
  json,
1721
- `Quote failed: ${msg}`,
1925
+ `Quote failed: ${apiErrorMessage(err)}`,
1722
1926
  "Check the coin address is valid and try again. Use --debug for full error details."
1723
1927
  );
1724
1928
  }
@@ -1747,8 +1951,11 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1747
1951
  coin_name: coinName,
1748
1952
  coin_symbol: coinSymbol,
1749
1953
  amount_mode: amountMode,
1954
+ swap_amount_usd: swapAmountUsd,
1955
+ valueUsd: swapAmountUsd,
1956
+ swapCoinType: token.coinType ?? null,
1750
1957
  slippage: slippagePct,
1751
- output_format: opts.output
1958
+ output_format: json ? "json" : "static"
1752
1959
  });
1753
1960
  return;
1754
1961
  }
@@ -1776,6 +1983,8 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1776
1983
  let receipt;
1777
1984
  let txHash;
1778
1985
  let receivedAmountOut = BigInt(amountOut);
1986
+ let swapLogIndex = null;
1987
+ const swapCoinType = token.coinType ?? null;
1779
1988
  try {
1780
1989
  receipt = await tradeCoin({
1781
1990
  tradeParameters,
@@ -1790,24 +1999,26 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1790
1999
  coin_name: coinName,
1791
2000
  coin_symbol: coinSymbol,
1792
2001
  amount_mode: amountMode,
2002
+ swap_amount_usd: swapAmountUsd,
2003
+ valueUsd: swapAmountUsd,
2004
+ swapCoinType,
1793
2005
  slippage: slippagePct,
1794
- output_format: opts.output,
2006
+ output_format: json ? "json" : "static",
1795
2007
  success: false,
1796
2008
  error_type: err instanceof Error ? err.constructor.name : "unknown"
1797
2009
  });
1798
2010
  await shutdownAnalytics();
1799
- outputErrorAndExit(
1800
- json,
1801
- `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
1802
- );
2011
+ outputErrorAndExit(json, tradeErrorMessage(err));
1803
2012
  }
1804
2013
  txHash = receipt.transactionHash;
1805
2014
  try {
1806
- receivedAmountOut = getReceivedAmountFromReceipt({
2015
+ const result = getReceivedAmountFromReceipt({
1807
2016
  receipt,
1808
2017
  tokenAddress: coinAddress,
1809
2018
  recipient: account.address
1810
2019
  });
2020
+ receivedAmountOut = result.amount;
2021
+ swapLogIndex = result.logIndex;
1811
2022
  } catch (err) {
1812
2023
  console.warn(
1813
2024
  `Warning: transaction succeeded but could not determine received amount: ${err instanceof Error ? err.message : String(err)}`
@@ -1833,13 +2044,518 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
1833
2044
  amount_mode: amountMode,
1834
2045
  input_amount: amountIn.toString(),
1835
2046
  input_token_symbol: inputToken.symbol,
2047
+ swap_amount_usd: swapAmountUsd,
2048
+ valueUsd: swapAmountUsd,
2049
+ swapCoinType,
2050
+ transactionHash: txHash,
2051
+ logIndex: swapLogIndex,
1836
2052
  slippage: slippagePct,
1837
- output_format: opts.output,
2053
+ output_format: json ? "json" : "static",
1838
2054
  success: true,
1839
2055
  tx_hash: txHash
1840
2056
  });
1841
2057
  });
1842
2058
 
2059
+ // src/commands/explore.tsx
2060
+ import { Command as Command4 } from "commander";
2061
+ import {
2062
+ setApiKey as setApiKey3,
2063
+ getCoinsTopVolume24h,
2064
+ getCoinsMostValuable,
2065
+ getCoinsNew,
2066
+ getCoinsTopGainers,
2067
+ getCoinsLastTraded,
2068
+ getCoinsLastTradedUnique,
2069
+ getExploreTopVolumeAll24h,
2070
+ getExploreTopVolumeCreators24h,
2071
+ getExploreNewAll,
2072
+ getExploreFeaturedCreators,
2073
+ getExploreFeaturedVideos,
2074
+ getCreatorCoins,
2075
+ getMostValuableCreatorCoins,
2076
+ getMostValuableAll,
2077
+ getMostValuableTrends,
2078
+ getNewTrends,
2079
+ getTopVolumeTrends24h,
2080
+ getTrendingAll,
2081
+ getTrendingCreators,
2082
+ getTrendingPosts,
2083
+ getTrendingTrends
2084
+ } from "@zoralabs/coins-sdk";
2085
+
2086
+ // src/lib/types.ts
2087
+ var SORT_LABELS2 = {
2088
+ mcap: "Top by Market Cap",
2089
+ volume: "Top by 24h Volume",
2090
+ new: "New",
2091
+ gainers: "Top Gainers (24h)",
2092
+ "last-traded": "Last Traded",
2093
+ "last-traded-unique": "Last Traded (Unique)",
2094
+ trending: "Trending",
2095
+ featured: "Featured"
2096
+ };
2097
+ var TYPE_LABELS = {
2098
+ all: "all",
2099
+ trend: "trends",
2100
+ "creator-coin": "creator coins",
2101
+ post: "posts"
2102
+ };
2103
+ var COIN_TYPE_DISPLAY = {
2104
+ CONTENT: "post",
2105
+ CREATOR: "creator-coin",
2106
+ TREND: "trend"
2107
+ };
2108
+
2109
+ // src/components/ExploreView.tsx
2110
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef2 } from "react";
2111
+ import { Box as Box4, Text as Text4, useInput as useInput2, useApp as useApp2 } from "ink";
2112
+ import Spinner2 from "ink-spinner";
2113
+
2114
+ // src/lib/clipboard.ts
2115
+ import { execFileSync } from "child_process";
2116
+ import { platform as platform2 } from "os";
2117
+ var copyToClipboard = (text) => {
2118
+ const os = platform2();
2119
+ try {
2120
+ if (os === "darwin") {
2121
+ execFileSync("pbcopy", {
2122
+ input: text,
2123
+ stdio: ["pipe", "ignore", "ignore"]
2124
+ });
2125
+ } else if (os === "linux") {
2126
+ execFileSync("xclip", ["-selection", "clipboard"], {
2127
+ input: text,
2128
+ stdio: ["pipe", "ignore", "ignore"]
2129
+ });
2130
+ } else if (os === "win32") {
2131
+ execFileSync("clip", {
2132
+ input: text,
2133
+ stdio: ["pipe", "ignore", "ignore"]
2134
+ });
2135
+ } else {
2136
+ return false;
2137
+ }
2138
+ return true;
2139
+ } catch {
2140
+ return false;
2141
+ }
2142
+ };
2143
+
2144
+ // src/components/ExploreView.tsx
2145
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2146
+ var COLUMNS = [
2147
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
2148
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2149
+ {
2150
+ header: "Address",
2151
+ width: 14,
2152
+ accessor: (c) => c.address ? truncateAddress(c.address) : ""
2153
+ },
2154
+ {
2155
+ header: "Type",
2156
+ width: 14,
2157
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
2158
+ },
2159
+ {
2160
+ header: "Market Cap",
2161
+ width: 12,
2162
+ accessor: (c) => formatCompactUsd(c.marketCap)
2163
+ },
2164
+ {
2165
+ header: "24h Vol",
2166
+ width: 12,
2167
+ accessor: (c) => formatCompactUsd(c.volume24h)
2168
+ },
2169
+ {
2170
+ header: "24h Change",
2171
+ width: 11,
2172
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
2173
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
2174
+ }
2175
+ ];
2176
+ var CACHE_TTL_MS = 6e4;
2177
+ var CACHE_KEY_FIRST = "__first__";
2178
+ var ExploreView = ({
2179
+ fetchPage,
2180
+ sort,
2181
+ type,
2182
+ limit,
2183
+ initialCursor,
2184
+ cacheTtlMs = CACHE_TTL_MS,
2185
+ autoRefresh = false,
2186
+ intervalSeconds = 30
2187
+ }) => {
2188
+ const { exit } = useApp2();
2189
+ const [loading, setLoading] = useState3(true);
2190
+ const [error, setError] = useState3(null);
2191
+ const [coins, setCoins] = useState3([]);
2192
+ const [pageInfo, setPageInfo] = useState3(null);
2193
+ const [page, setPage] = useState3(1);
2194
+ const [cursorHistory, setCursorHistory] = useState3(
2195
+ []
2196
+ );
2197
+ const [currentCursor, setCurrentCursor] = useState3(
2198
+ initialCursor
2199
+ );
2200
+ const cache = useRef2(/* @__PURE__ */ new Map());
2201
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
2202
+ const [manualRefreshCount, setManualRefreshCount] = useState3(0);
2203
+ const [selectedRow, setSelectedRow] = useState3(0);
2204
+ const [copyFeedback, setCopyFeedback] = useState3(null);
2205
+ useEffect3(() => {
2206
+ setSelectedRow((r) => Math.min(r, Math.max(0, coins.length - 1)));
2207
+ }, [coins.length]);
2208
+ const loadPage = useCallback3(
2209
+ async (cursor) => {
2210
+ const cacheKey = cursor ?? CACHE_KEY_FIRST;
2211
+ const cached = cache.current.get(cacheKey);
2212
+ const isFresh = cached && Date.now() - cached.fetchedAt < cacheTtlMs;
2213
+ if (isFresh) {
2214
+ setCoins(cached.result.coins);
2215
+ setPageInfo(cached.result.pageInfo ?? null);
2216
+ setError(null);
2217
+ setLoading(false);
2218
+ return;
2219
+ }
2220
+ setLoading(true);
2221
+ setError(null);
2222
+ try {
2223
+ const result = await fetchPage(cursor);
2224
+ cache.current.set(cacheKey, { result, fetchedAt: Date.now() });
2225
+ setCoins(result.coins);
2226
+ setPageInfo(result.pageInfo ?? null);
2227
+ } catch (err) {
2228
+ setError(err instanceof Error ? err.message : String(err));
2229
+ }
2230
+ setLoading(false);
2231
+ },
2232
+ [fetchPage, cacheTtlMs]
2233
+ );
2234
+ useEffect3(() => {
2235
+ if (refreshCount === 0) return;
2236
+ const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
2237
+ cache.current.delete(cacheKey);
2238
+ }, [refreshCount, currentCursor]);
2239
+ useEffect3(() => {
2240
+ loadPage(currentCursor);
2241
+ }, [currentCursor, loadPage, refreshCount, manualRefreshCount]);
2242
+ useInput2((input, key) => {
2243
+ if (input === "q" || key.escape) {
2244
+ exit();
2245
+ return;
2246
+ }
2247
+ if (loading) return;
2248
+ if (key.upArrow || input === "k") {
2249
+ setSelectedRow((r) => Math.max(0, r - 1));
2250
+ return;
2251
+ }
2252
+ if (key.downArrow || input === "j") {
2253
+ setSelectedRow((r) => Math.min(coins.length - 1, r + 1));
2254
+ return;
2255
+ }
2256
+ if (input === "c") {
2257
+ const coin = coins[selectedRow];
2258
+ if (coin?.address) {
2259
+ const ok = copyToClipboard(coin.address);
2260
+ setCopyFeedback(ok ? "Copied!" : "Copy failed");
2261
+ setTimeout(() => setCopyFeedback(null), 1500);
2262
+ }
2263
+ return;
2264
+ }
2265
+ const canGoNext = pageInfo?.hasNextPage && pageInfo.endCursor;
2266
+ const canGoPrev = cursorHistory.length > 0;
2267
+ if ((input === "n" || key.rightArrow) && canGoNext) {
2268
+ setCursorHistory((prev) => [...prev, currentCursor]);
2269
+ setCurrentCursor(pageInfo.endCursor);
2270
+ setPage((p) => p + 1);
2271
+ setSelectedRow(0);
2272
+ }
2273
+ if ((input === "p" || key.leftArrow) && canGoPrev) {
2274
+ const prev = cursorHistory[cursorHistory.length - 1];
2275
+ setCursorHistory((h) => h.slice(0, -1));
2276
+ setCurrentCursor(prev);
2277
+ setPage((p) => p - 1);
2278
+ setSelectedRow(0);
2279
+ }
2280
+ if (input === "r") {
2281
+ const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
2282
+ cache.current.delete(cacheKey);
2283
+ triggerManualRefresh();
2284
+ setManualRefreshCount((c) => c + 1);
2285
+ setSelectedRow(0);
2286
+ }
2287
+ });
2288
+ if (error) {
2289
+ return /* @__PURE__ */ jsxs4(
2290
+ Box4,
2291
+ {
2292
+ flexDirection: "column",
2293
+ paddingLeft: 1,
2294
+ paddingTop: 1,
2295
+ paddingBottom: 1,
2296
+ children: [
2297
+ /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
2298
+ "Error: ",
2299
+ error
2300
+ ] }),
2301
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
2302
+ ]
2303
+ }
2304
+ );
2305
+ }
2306
+ if (loading) {
2307
+ return /* @__PURE__ */ jsx4(Box4, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { children: [
2308
+ /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
2309
+ " Loading\u2026"
2310
+ ] }) });
2311
+ }
2312
+ if (coins.length === 0) {
2313
+ return /* @__PURE__ */ jsxs4(
2314
+ Box4,
2315
+ {
2316
+ flexDirection: "column",
2317
+ paddingLeft: 1,
2318
+ paddingTop: 1,
2319
+ paddingBottom: 1,
2320
+ children: [
2321
+ /* @__PURE__ */ jsx4(Text4, { children: "No coins found." }),
2322
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
2323
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Try a different sort or type:" }),
2324
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " zora explore --sort volume --type all" }),
2325
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " zora explore --sort new --type all" })
2326
+ ] }),
2327
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
2328
+ ]
2329
+ }
2330
+ );
2331
+ }
2332
+ const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
2333
+ const subtitle = `Page ${page} \xB7 ${coins.length} result${coins.length !== 1 ? "s" : ""}`;
2334
+ const rankedCoins = coins.map((c, i) => ({
2335
+ ...c,
2336
+ rank: (page - 1) * limit + i + 1
2337
+ }));
2338
+ const hints = [];
2339
+ hints.push("\u2191\u2193 select");
2340
+ hints.push("c copy address");
2341
+ if (cursorHistory.length > 0) hints.push("\u2190 prev");
2342
+ if (pageInfo?.hasNextPage) hints.push("\u2192 next");
2343
+ hints.push("r refresh");
2344
+ if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
2345
+ hints.push("q quit");
2346
+ const footer = hints.join(" \xB7 ") + (copyFeedback ? ` ${copyFeedback}` : "");
2347
+ return /* @__PURE__ */ jsx4(
2348
+ Table,
2349
+ {
2350
+ data: rankedCoins,
2351
+ columns: COLUMNS,
2352
+ title,
2353
+ subtitle,
2354
+ footer,
2355
+ selectedRow
2356
+ }
2357
+ );
2358
+ };
2359
+
2360
+ // src/commands/explore.tsx
2361
+ import { jsx as jsx5 } from "react/jsx-runtime";
2362
+ var QUERY_MAP = {
2363
+ mcap: {
2364
+ all: getMostValuableAll,
2365
+ trend: getMostValuableTrends,
2366
+ "creator-coin": getMostValuableCreatorCoins,
2367
+ post: getCoinsMostValuable
2368
+ },
2369
+ volume: {
2370
+ all: getExploreTopVolumeAll24h,
2371
+ trend: getTopVolumeTrends24h,
2372
+ "creator-coin": getExploreTopVolumeCreators24h,
2373
+ post: getCoinsTopVolume24h
2374
+ },
2375
+ new: {
2376
+ all: getExploreNewAll,
2377
+ trend: getNewTrends,
2378
+ "creator-coin": getCreatorCoins,
2379
+ post: getCoinsNew
2380
+ },
2381
+ gainers: {
2382
+ post: getCoinsTopGainers
2383
+ },
2384
+ "last-traded": {
2385
+ post: getCoinsLastTraded
2386
+ },
2387
+ "last-traded-unique": {
2388
+ post: getCoinsLastTradedUnique
2389
+ },
2390
+ trending: {
2391
+ all: getTrendingAll,
2392
+ trend: getTrendingTrends,
2393
+ "creator-coin": getTrendingCreators,
2394
+ post: getTrendingPosts
2395
+ },
2396
+ featured: {
2397
+ "creator-coin": getExploreFeaturedCreators,
2398
+ post: getExploreFeaturedVideos
2399
+ }
2400
+ };
2401
+ var STATIC_COLUMNS = [
2402
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
2403
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2404
+ { header: "Address", width: 48, accessor: (c) => c.address ?? "" },
2405
+ {
2406
+ header: "Type",
2407
+ width: 14,
2408
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
2409
+ },
2410
+ {
2411
+ header: "Market Cap",
2412
+ width: 12,
2413
+ accessor: (c) => formatCompactUsd(c.marketCap)
2414
+ },
2415
+ {
2416
+ header: "24h Vol",
2417
+ width: 12,
2418
+ accessor: (c) => formatCompactUsd(c.volume24h)
2419
+ },
2420
+ {
2421
+ header: "24h Change",
2422
+ width: 11,
2423
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
2424
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
2425
+ }
2426
+ ];
2427
+ var SORT_OPTIONS2 = Object.keys(SORT_LABELS2).join(", ");
2428
+ var exploreCommand = new Command4("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "mcap").option(
2429
+ "--type <type>",
2430
+ "Filter by type: all, trend, creator-coin, post (availability varies by sort)",
2431
+ "post"
2432
+ ).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
2433
+ "--refresh <seconds>",
2434
+ "Auto-refresh interval in seconds, requires --live (min 5)",
2435
+ "30"
2436
+ ).action(async function(opts) {
2437
+ const output = getOutputMode(this, "live");
2438
+ const json = output === "json";
2439
+ const sort = opts.sort;
2440
+ const type = opts.type;
2441
+ const limit = parseInt(opts.limit, 10);
2442
+ const after = opts.after;
2443
+ if (isNaN(limit) || limit <= 0 || limit > 20) {
2444
+ outputErrorAndExit(
2445
+ json,
2446
+ `Invalid --limit value: ${opts.limit}. Must be an integer between 1 and 20.`,
2447
+ "Usage: zora explore --limit 10"
2448
+ );
2449
+ }
2450
+ if (!QUERY_MAP[sort]) {
2451
+ outputErrorAndExit(
2452
+ json,
2453
+ `Invalid --sort value: ${sort}.`,
2454
+ `Supported: ${SORT_OPTIONS2}`
2455
+ );
2456
+ }
2457
+ if (!QUERY_MAP[sort][type]) {
2458
+ const supported = Object.keys(QUERY_MAP[sort]);
2459
+ outputErrorAndExit(
2460
+ json,
2461
+ `Invalid --type for --sort ${sort}.`,
2462
+ `Supported: ${supported.join(", ")}`
2463
+ );
2464
+ }
2465
+ const apiKey = getApiKey();
2466
+ if (apiKey) {
2467
+ setApiKey3(apiKey);
2468
+ }
2469
+ const queryFn = QUERY_MAP[sort][type];
2470
+ if (json) {
2471
+ let response;
2472
+ try {
2473
+ response = await queryFn({ count: limit, after });
2474
+ } catch (err) {
2475
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2476
+ }
2477
+ if (response.error) {
2478
+ const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
2479
+ outputErrorAndExit(json, `API error: ${msg}`);
2480
+ }
2481
+ const edges = response.data?.exploreList?.edges ?? [];
2482
+ const coins = edges.map((e) => e.node);
2483
+ const pageInfo = response.data?.exploreList?.pageInfo;
2484
+ outputJson({ coins, pageInfo: pageInfo ?? null });
2485
+ track("cli_explore", {
2486
+ sort,
2487
+ type,
2488
+ limit,
2489
+ paginated: after !== void 0,
2490
+ result_count: coins.length,
2491
+ has_next_page: pageInfo?.hasNextPage ?? false,
2492
+ output_format: "json"
2493
+ });
2494
+ } else {
2495
+ const { live, intervalSeconds } = getLiveConfig(this, output);
2496
+ const fetchPage = async (cursor) => {
2497
+ const response = await queryFn({ count: limit, after: cursor });
2498
+ if (response.error) {
2499
+ const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
2500
+ throw new Error(msg);
2501
+ }
2502
+ const edges = response.data?.exploreList?.edges ?? [];
2503
+ const coins = edges.map((e) => e.node);
2504
+ const pageInfo = response.data?.exploreList?.pageInfo;
2505
+ return { coins, pageInfo };
2506
+ };
2507
+ if (live) {
2508
+ await renderLive(
2509
+ /* @__PURE__ */ jsx5(
2510
+ ExploreView,
2511
+ {
2512
+ fetchPage,
2513
+ sort,
2514
+ type,
2515
+ limit,
2516
+ initialCursor: after,
2517
+ autoRefresh: live,
2518
+ intervalSeconds
2519
+ }
2520
+ )
2521
+ );
2522
+ track("cli_explore", {
2523
+ sort,
2524
+ type,
2525
+ limit,
2526
+ live,
2527
+ interval: intervalSeconds,
2528
+ paginated: after !== void 0,
2529
+ output_format: "live"
2530
+ });
2531
+ } else {
2532
+ const { coins } = await fetchPage(after).catch(
2533
+ (err) => outputErrorAndExit(
2534
+ false,
2535
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
2536
+ )
2537
+ );
2538
+ const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
2539
+ const rankedCoins = coins.map((c, i) => ({
2540
+ ...c,
2541
+ rank: i + 1
2542
+ }));
2543
+ renderOnce(
2544
+ /* @__PURE__ */ jsx5(Table, { columns: STATIC_COLUMNS, data: rankedCoins, title })
2545
+ );
2546
+ track("cli_explore", {
2547
+ sort,
2548
+ type,
2549
+ limit,
2550
+ live: false,
2551
+ paginated: after !== void 0,
2552
+ result_count: coins.length,
2553
+ output_format: "static"
2554
+ });
2555
+ }
2556
+ }
2557
+ });
2558
+
1843
2559
  // src/commands/get.tsx
1844
2560
  import { Command as Command5 } from "commander";
1845
2561
  import { setApiKey as setApiKey4 } from "@zoralabs/coins-sdk";
@@ -1933,24 +2649,24 @@ async function resolveCoin(ref) {
1933
2649
  }
1934
2650
 
1935
2651
  // 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";
2652
+ import { Box as Box5, Text as Text5 } from "ink";
2653
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1938
2654
  var LABEL_WIDTH = 18;
1939
2655
  function Row({
1940
2656
  label,
1941
2657
  children
1942
2658
  }) {
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 })
2659
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
2660
+ /* @__PURE__ */ jsx6(Box5, { width: LABEL_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: label }) }),
2661
+ /* @__PURE__ */ jsx6(Text5, { children })
1946
2662
  ] });
1947
2663
  }
1948
2664
  function CoinDetail({ coin }) {
1949
2665
  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: [
2666
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingLeft: 1, children: [
2667
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
2668
+ /* @__PURE__ */ jsx6(Text5, { bold: true, children: coin.name }),
2669
+ /* @__PURE__ */ jsxs5(Text5, { children: [
1954
2670
  coin.coinType,
1955
2671
  " ",
1956
2672
  "\xB7",
@@ -1958,20 +2674,20 @@ function CoinDetail({ coin }) {
1958
2674
  coin.address
1959
2675
  ] })
1960
2676
  ] }),
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) })
2677
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
2678
+ /* @__PURE__ */ jsx6(Row, { label: "Market Cap", children: formatCompactUsd(coin.marketCap) }),
2679
+ /* @__PURE__ */ jsx6(Row, { label: "24h Volume", children: formatCompactUsd(coin.volume24h) }),
2680
+ /* @__PURE__ */ jsx6(Row, { label: "24h Change", children: /* @__PURE__ */ jsx6(Text5, { color: change.color, children: change.text }) }),
2681
+ /* @__PURE__ */ jsx6(Row, { label: "Holders", children: formatHolders(coin.uniqueHolders) }),
2682
+ coin.coinType === "post" && (coin.creatorHandle ?? coin.creatorAddress) && /* @__PURE__ */ jsx6(Row, { label: "Creator", children: coin.creatorHandle ?? coin.creatorAddress }),
2683
+ /* @__PURE__ */ jsx6(Row, { label: "Created", children: formatCreatedAt(coin.createdAt) })
1968
2684
  ] }),
1969
- /* @__PURE__ */ jsx3(Box3, { marginBottom: 1 })
2685
+ /* @__PURE__ */ jsx6(Box5, { marginBottom: 1 })
1970
2686
  ] });
1971
2687
  }
1972
2688
 
1973
2689
  // src/commands/get.tsx
1974
- import { jsx as jsx4 } from "react/jsx-runtime";
2690
+ import { jsx as jsx7 } from "react/jsx-runtime";
1975
2691
  function formatCoinJson(coin) {
1976
2692
  return {
1977
2693
  name: coin.name,
@@ -1987,7 +2703,7 @@ function formatCoinJson(coin) {
1987
2703
  };
1988
2704
  }
1989
2705
  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) {
2706
+ 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
2707
  const json = getJson(this);
1992
2708
  if (opts.type !== void 0 && !VALID_TYPES.includes(opts.type)) {
1993
2709
  outputErrorAndExit(
@@ -2013,10 +2729,7 @@ var getCommand = new Command5("get").description("Look up a coin by address or n
2013
2729
  try {
2014
2730
  result = await resolveCoin(ref);
2015
2731
  } catch (err) {
2016
- outputErrorAndExit(
2017
- json,
2018
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2019
- );
2732
+ outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2020
2733
  return;
2021
2734
  }
2022
2735
  if (type && result.kind === "found" && result.coin.coinType !== type) {
@@ -2033,8 +2746,8 @@ var getCommand = new Command5("get").description("Look up a coin by address or n
2033
2746
  }
2034
2747
  outputData(json, {
2035
2748
  json: formatCoinJson(result.coin),
2036
- table: () => {
2037
- renderOnce(/* @__PURE__ */ jsx4(CoinDetail, { coin: result.coin }));
2749
+ render: () => {
2750
+ renderOnce(/* @__PURE__ */ jsx7(CoinDetail, { coin: result.coin }));
2038
2751
  }
2039
2752
  });
2040
2753
  track("cli_get", {
@@ -2046,19 +2759,243 @@ var getCommand = new Command5("get").description("Look up a coin by address or n
2046
2759
  });
2047
2760
  });
2048
2761
 
2049
- // src/commands/sell.ts
2762
+ // src/commands/price-history.tsx
2050
2763
  import { Command as Command6 } from "commander";
2051
- import confirm3 from "@inquirer/confirm";
2052
- import {
2053
- erc20Abi as erc20Abi3,
2054
- formatUnits as formatUnits4,
2764
+ import { setApiKey as setApiKey5, apiGet } from "@zoralabs/coins-sdk";
2765
+
2766
+ // src/components/PriceHistory.tsx
2767
+ import { Box as Box6, Text as Text6 } from "ink";
2768
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2769
+ var LABEL_WIDTH2 = 18;
2770
+ var Row2 = ({
2771
+ label,
2772
+ children
2773
+ }) => /* @__PURE__ */ jsxs6(Box6, { children: [
2774
+ /* @__PURE__ */ jsx8(Box6, { width: LABEL_WIDTH2, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: label }) }),
2775
+ /* @__PURE__ */ jsx8(Text6, { children })
2776
+ ] });
2777
+ var PriceHistory = ({
2778
+ coin,
2779
+ coinType,
2780
+ interval,
2781
+ high,
2782
+ low,
2783
+ change,
2784
+ sparklineText
2785
+ }) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 1, children: [
2786
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
2787
+ /* @__PURE__ */ jsx8(Row2, { label: "Coin", children: coin }),
2788
+ /* @__PURE__ */ jsx8(Row2, { label: "Type", children: coinType }),
2789
+ /* @__PURE__ */ jsx8(Row2, { label: "Interval", children: interval }),
2790
+ /* @__PURE__ */ jsx8(Row2, { label: "High", children: high }),
2791
+ /* @__PURE__ */ jsx8(Row2, { label: "Low", children: low }),
2792
+ /* @__PURE__ */ jsx8(Row2, { label: "Change", children: /* @__PURE__ */ jsx8(Text6, { color: change.color, children: change.text }) })
2793
+ ] }),
2794
+ sparklineText.length > 0 && /* @__PURE__ */ jsx8(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text6, { children: sparklineText }) }),
2795
+ /* @__PURE__ */ jsx8(Box6, { marginBottom: 1 })
2796
+ ] });
2797
+
2798
+ // src/lib/sparkline.ts
2799
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2800
+ var sparkline = (values) => {
2801
+ if (values.length <= 1) return values.length === 1 ? BLOCKS[4] : "";
2802
+ const min = Math.min(...values);
2803
+ const max = Math.max(...values);
2804
+ const range = max - min;
2805
+ if (range === 0) return BLOCKS[4].repeat(values.length);
2806
+ return values.map((v) => {
2807
+ const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
2808
+ return BLOCKS[idx];
2809
+ }).join("");
2810
+ };
2811
+ var MAX_SPARKLINE_WIDTH = 50;
2812
+ var downsample = (values, maxWidth) => {
2813
+ if (values.length <= maxWidth) return values;
2814
+ const bucketSize = values.length / maxWidth;
2815
+ const result = [];
2816
+ for (let i = 0; i < maxWidth; i++) {
2817
+ const start = Math.floor(i * bucketSize);
2818
+ const end = Math.floor((i + 1) * bucketSize);
2819
+ let sum = 0;
2820
+ for (let j = start; j < end; j++) {
2821
+ sum += values[j];
2822
+ }
2823
+ result.push(sum / (end - start));
2824
+ }
2825
+ return result;
2826
+ };
2827
+
2828
+ // src/commands/price-history.tsx
2829
+ import { jsx as jsx9 } from "react/jsx-runtime";
2830
+ var VALID_TYPES2 = ["creator-coin", "post", "trend"];
2831
+ var VALID_INTERVALS = ["1h", "24h", "1w", "1m", "ALL"];
2832
+ var INTERVAL_TO_API_FIELD = {
2833
+ "1h": "oneHour",
2834
+ "24h": "oneDay",
2835
+ "1w": "oneWeek",
2836
+ "1m": "oneMonth",
2837
+ ALL: "all"
2838
+ };
2839
+ var formatPrice = (price) => {
2840
+ if (price >= 1) {
2841
+ return `$${price.toFixed(2)}`;
2842
+ }
2843
+ if (price >= 0.01) {
2844
+ return `$${price.toFixed(4)}`;
2845
+ }
2846
+ return `$${price.toPrecision(4)}`;
2847
+ };
2848
+ var formatChange = (first, last) => {
2849
+ if (first === 0) return { text: "-", color: void 0 };
2850
+ const pct = (last - first) / first * 100;
2851
+ const prefix = pct >= 0 ? "+" : "";
2852
+ const text = `${prefix}${pct.toFixed(1)}%`;
2853
+ const color = pct > 0 ? "green" : pct < 0 ? "red" : void 0;
2854
+ return { text, color };
2855
+ };
2856
+ var fetchPriceHistory = async (address, interval) => {
2857
+ const response = await apiGet("/coinPriceHistory", {
2858
+ address
2859
+ });
2860
+ const data = response.data;
2861
+ const token = data?.zora20Token;
2862
+ if (!token) return [];
2863
+ const field = INTERVAL_TO_API_FIELD[interval];
2864
+ const points = token[field];
2865
+ if (!points || points.length === 0) return [];
2866
+ return points.map((p) => ({
2867
+ timestamp: p.timestamp,
2868
+ price: Number(p.closePrice)
2869
+ }));
2870
+ };
2871
+ var priceHistoryCommand = new Command6("price-history").description("Display price history for a coin").argument("[identifier]", "Coin address (0x...) or name").option("--type <type>", "Coin type: creator-coin, post, trend").option(
2872
+ "--interval <interval>",
2873
+ `Time range: ${VALID_INTERVALS.join(", ")}`,
2874
+ "1w"
2875
+ ).action(async function(identifier, opts) {
2876
+ const json = getJson(this);
2877
+ const interval = opts.interval ?? "1w";
2878
+ if (!VALID_INTERVALS.includes(interval)) {
2879
+ outputErrorAndExit(
2880
+ json,
2881
+ `Invalid --interval value: ${interval}.`,
2882
+ `Supported: ${VALID_INTERVALS.join(", ")}`
2883
+ );
2884
+ }
2885
+ if (opts.type !== void 0 && !VALID_TYPES2.includes(opts.type)) {
2886
+ outputErrorAndExit(
2887
+ json,
2888
+ `Invalid --type value: ${opts.type}.`,
2889
+ `Supported: ${VALID_TYPES2.join(", ")}`
2890
+ );
2891
+ }
2892
+ if (opts.type === "post" && !identifier.startsWith("0x")) {
2893
+ outputErrorAndExit(
2894
+ json,
2895
+ "Posts can only be looked up by address.",
2896
+ "Use: zora price-history 0x..."
2897
+ );
2898
+ }
2899
+ const ref = parseCoinRef(identifier, opts.type);
2900
+ const apiKey = getApiKey();
2901
+ if (apiKey) {
2902
+ setApiKey5(apiKey);
2903
+ }
2904
+ let result;
2905
+ try {
2906
+ result = await resolveCoin(ref);
2907
+ } catch (err) {
2908
+ outputErrorAndExit(
2909
+ json,
2910
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
2911
+ );
2912
+ return;
2913
+ }
2914
+ if (result.kind === "not-found") {
2915
+ outputErrorAndExit(json, result.message, result.suggestion);
2916
+ return;
2917
+ }
2918
+ const { coin } = result;
2919
+ let prices;
2920
+ try {
2921
+ prices = await fetchPriceHistory(coin.address, interval);
2922
+ } catch (err) {
2923
+ outputErrorAndExit(
2924
+ json,
2925
+ `Failed to fetch price data: ${err instanceof Error ? err.message : String(err)}`
2926
+ );
2927
+ return;
2928
+ }
2929
+ if (prices.length === 0) {
2930
+ outputErrorAndExit(
2931
+ json,
2932
+ `No price data found for ${coin.name} in the last ${interval}.`,
2933
+ "Try a longer interval with --interval"
2934
+ );
2935
+ return;
2936
+ }
2937
+ const priceValues = prices.map((p) => p.price);
2938
+ const high = Math.max(...priceValues);
2939
+ const low = Math.min(...priceValues);
2940
+ const change = formatChange(
2941
+ priceValues[0],
2942
+ priceValues[priceValues.length - 1]
2943
+ );
2944
+ const sparklineText = sparkline(
2945
+ downsample(priceValues, MAX_SPARKLINE_WIDTH)
2946
+ );
2947
+ outputData(json, {
2948
+ json: {
2949
+ coin: coin.name,
2950
+ type: coin.coinType,
2951
+ interval,
2952
+ high,
2953
+ low,
2954
+ change: priceValues[0] === 0 ? null : (priceValues[priceValues.length - 1] - priceValues[0]) / priceValues[0],
2955
+ prices: prices.map((p) => ({
2956
+ timestamp: p.timestamp,
2957
+ price: p.price
2958
+ }))
2959
+ },
2960
+ render: () => {
2961
+ renderOnce(
2962
+ /* @__PURE__ */ jsx9(
2963
+ PriceHistory,
2964
+ {
2965
+ coin: coin.name,
2966
+ coinType: coin.coinType,
2967
+ interval,
2968
+ high: formatPrice(high),
2969
+ low: formatPrice(low),
2970
+ change,
2971
+ sparklineText
2972
+ }
2973
+ )
2974
+ );
2975
+ }
2976
+ });
2977
+ track("cli_price_history", {
2978
+ lookup_type: identifier.startsWith("0x") ? "address" : "name",
2979
+ coin_type: coin.coinType,
2980
+ interval,
2981
+ data_points: prices.length,
2982
+ output_format: json ? "json" : "text"
2983
+ });
2984
+ });
2985
+
2986
+ // src/commands/sell.ts
2987
+ import { Command as Command7 } from "commander";
2988
+ import confirm3 from "@inquirer/confirm";
2989
+ import {
2990
+ erc20Abi as erc20Abi3,
2991
+ formatUnits as formatUnits4,
2055
2992
  isAddress as isAddress2,
2056
2993
  parseUnits as parseUnits2
2057
2994
  } from "viem";
2058
2995
  import {
2059
2996
  createTradeCall as createTradeCall2,
2060
2997
  getCoin as getCoin3,
2061
- setApiKey as setApiKey5,
2998
+ setApiKey as setApiKey6,
2062
2999
  tradeCoin as tradeCoin2
2063
3000
  } from "@zoralabs/coins-sdk";
2064
3001
  function printSellQuote(output, info) {
@@ -2133,19 +3070,13 @@ function printSellResult(output, info) {
2133
3070
  console.log(` Tx ${info.txHash}
2134
3071
  `);
2135
3072
  }
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";
3073
+ var sellCommand = new Command7("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").action(async function(coinAddress, opts) {
3074
+ const json = getJson(this);
2138
3075
  const debug = opts.debug === true;
2139
3076
  if (!isAddress2(coinAddress)) {
2140
3077
  outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
2141
3078
  }
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
- }
3079
+ const output = json ? "json" : "static";
2149
3080
  const outputAsset = opts.token ? opts.token.toLowerCase() : opts.to;
2150
3081
  if (!(outputAsset in BASE_TRADE_TOKENS)) {
2151
3082
  outputErrorAndExit(
@@ -2170,7 +3101,7 @@ var sellCommand = new Command6("sell").description("Sell a coin").argument("<add
2170
3101
  const slippage = slippagePct / 100;
2171
3102
  const apiKey = getApiKey();
2172
3103
  if (apiKey) {
2173
- setApiKey5(apiKey);
3104
+ setApiKey6(apiKey);
2174
3105
  }
2175
3106
  const account = resolveAccount(json);
2176
3107
  const { publicClient, walletClient } = createClients(account);
@@ -2179,10 +3110,7 @@ var sellCommand = new Command6("sell").description("Sell a coin").argument("<add
2179
3110
  const response = await getCoin3({ address: coinAddress });
2180
3111
  token = response.data?.zora20Token;
2181
3112
  } catch (err) {
2182
- outputErrorAndExit(
2183
- json,
2184
- `Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
2185
- );
3113
+ outputErrorAndExit(json, `Failed to fetch coin: ${apiErrorMessage(err)}`);
2186
3114
  }
2187
3115
  if (!token) {
2188
3116
  outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
@@ -2200,52 +3128,995 @@ var sellCommand = new Command6("sell").description("Sell a coin").argument("<add
2200
3128
  );
2201
3129
  return;
2202
3130
  }
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.`
3131
+ const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
3132
+ if (coinPriceUsd === null || coinPriceUsd <= 0) {
3133
+ outputErrorAndExit(
3134
+ json,
3135
+ `Failed to fetch ${coinSymbol} price for USD conversion.`
3136
+ );
3137
+ return;
3138
+ }
3139
+ const coinAmount = usdVal / coinPriceUsd;
3140
+ amountIn = parseUnits2(coinAmount.toFixed(coinDecimals), coinDecimals);
3141
+ if (amountIn === 0n) {
3142
+ outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
3143
+ }
3144
+ if (debug) {
3145
+ console.error(
3146
+ `[debug] $${usdVal} USD = ${formatUnits4(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
3147
+ );
3148
+ }
3149
+ } else if (amountMode === "amount") {
3150
+ const val = parsePercentageLikeValue(opts.amount);
3151
+ if (val === void 0 || val <= 0) {
3152
+ outputErrorAndExit(
3153
+ json,
3154
+ "Invalid --amount value. Must be a positive number."
3155
+ );
3156
+ }
3157
+ try {
3158
+ amountIn = parseUnits2(opts.amount, coinDecimals);
3159
+ } catch {
3160
+ outputErrorAndExit(json, "Invalid --amount value for token decimals.");
3161
+ }
3162
+ } else {
3163
+ const balance = await publicClient.readContract({
3164
+ abi: erc20Abi3,
3165
+ address: coinAddress,
3166
+ functionName: "balanceOf",
3167
+ args: [account.address]
3168
+ });
3169
+ if (balance === 0n) {
3170
+ outputErrorAndExit(
3171
+ json,
3172
+ `No ${coinSymbol} balance. Buy some first or pick a different wallet.`
3173
+ );
3174
+ }
3175
+ if (amountMode === "all") {
3176
+ amountIn = balance;
3177
+ } else {
3178
+ const pct = parsePercentageLikeValue(opts.percent);
3179
+ if (pct === void 0 || pct <= 0 || pct > 100) {
3180
+ outputErrorAndExit(
3181
+ json,
3182
+ "Invalid --percent value. Must be between 0 and 100."
3183
+ );
3184
+ }
3185
+ amountIn = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
3186
+ if (amountIn === 0n) {
3187
+ outputErrorAndExit(
3188
+ json,
3189
+ "Calculated amount is zero. Balance too low."
3190
+ );
3191
+ }
3192
+ }
3193
+ }
3194
+ let swapAmountUsd;
3195
+ if (amountMode === "usd") {
3196
+ swapAmountUsd = parsePercentageLikeValue(opts.usd);
3197
+ } else {
3198
+ const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
3199
+ if (coinPriceUsd !== null && coinPriceUsd > 0) {
3200
+ swapAmountUsd = Number(
3201
+ (Number(formatUnits4(amountIn, coinDecimals)) * coinPriceUsd).toFixed(
3202
+ 2
3203
+ )
3204
+ );
3205
+ }
3206
+ }
3207
+ const tradeParameters = {
3208
+ sell: { type: "erc20", address: coinAddress },
3209
+ buy: outputToken.trade,
3210
+ amountIn,
3211
+ slippage,
3212
+ sender: account.address
3213
+ };
3214
+ if (debug) {
3215
+ printDebugRequest("sell", tradeParameters);
3216
+ }
3217
+ let quoteAmountOut;
3218
+ try {
3219
+ const quote = await createTradeCall2(tradeParameters);
3220
+ if (debug) {
3221
+ printDebugResponse("sell", quote);
3222
+ }
3223
+ if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
3224
+ outputErrorAndExit(
3225
+ json,
3226
+ "Quote returned zero output. Amount may be too small."
3227
+ );
3228
+ }
3229
+ quoteAmountOut = quote.quote.amountOut;
3230
+ } catch (err) {
3231
+ if (debug) {
3232
+ console.error(
3233
+ `
3234
+ [debug] sell \u2014 Quote Error:
3235
+ ${err instanceof Error ? err.stack || err.message : String(err)}
3236
+ `
3237
+ );
3238
+ }
3239
+ const msg = err instanceof Error ? err.message : String(err);
3240
+ const errorType = err?.errorType;
3241
+ const errorBody = err?.errorBody;
3242
+ if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
3243
+ if (json) {
3244
+ outputJson({ error: errorBody ?? msg });
3245
+ process.exit(1);
3246
+ }
3247
+ outputErrorAndExit(
3248
+ json,
3249
+ "Not enough available liquidity for your swap. Please try swapping fewer tokens."
3250
+ );
3251
+ }
3252
+ outputErrorAndExit(
3253
+ json,
3254
+ `Quote failed: ${apiErrorMessage(err)}`,
3255
+ "Check the coin address and amount, then try again. Use --debug for full error details."
3256
+ );
3257
+ }
3258
+ const soldFormatted = formatAmountDisplay(amountIn, coinDecimals);
3259
+ const receivedFormatted = formatAmountDisplay(
3260
+ BigInt(quoteAmountOut),
3261
+ outputToken.decimals
3262
+ );
3263
+ if (opts.quote) {
3264
+ printSellQuote(output, {
3265
+ coinName,
3266
+ coinSymbol,
3267
+ address: coinAddress,
3268
+ soldFormatted,
3269
+ amountIn,
3270
+ coinDecimals,
3271
+ receivedFormatted,
3272
+ quoteAmountOut,
3273
+ outputSymbol: outputToken.symbol,
3274
+ outputDecimals: outputToken.decimals,
3275
+ slippagePct
3276
+ });
3277
+ track("cli_sell", {
3278
+ action: "quote",
3279
+ coin_address: coinAddress,
3280
+ coin_name: coinName,
3281
+ coin_symbol: coinSymbol,
3282
+ amount_mode: amountMode,
3283
+ swap_amount_usd: swapAmountUsd,
3284
+ valueUsd: swapAmountUsd,
3285
+ swapCoinType: token.coinType ?? null,
3286
+ output_asset: outputAsset,
3287
+ slippage: slippagePct,
3288
+ output_format: output
3289
+ });
3290
+ return;
3291
+ }
3292
+ if (!opts.yes) {
3293
+ printSellQuote("static", {
3294
+ coinName,
3295
+ coinSymbol,
3296
+ address: coinAddress,
3297
+ soldFormatted,
3298
+ amountIn,
3299
+ coinDecimals,
3300
+ receivedFormatted,
3301
+ quoteAmountOut,
3302
+ outputSymbol: outputToken.symbol,
3303
+ outputDecimals: outputToken.decimals,
3304
+ slippagePct
3305
+ });
3306
+ const ok = await confirm3({
3307
+ message: "Confirm?",
3308
+ default: false
3309
+ });
3310
+ if (!ok) {
3311
+ console.error("Aborted.");
3312
+ process.exit(0);
3313
+ }
3314
+ }
3315
+ let receipt;
3316
+ let txHash;
3317
+ let receivedAmountOut = BigInt(quoteAmountOut);
3318
+ let receivedSource = "quote";
3319
+ let swapLogIndex = null;
3320
+ const swapCoinType = token.coinType ?? null;
3321
+ try {
3322
+ receipt = await tradeCoin2({
3323
+ tradeParameters,
3324
+ walletClient,
3325
+ publicClient,
3326
+ account
3327
+ });
3328
+ } catch (err) {
3329
+ track("cli_sell", {
3330
+ action: "trade",
3331
+ coin_address: coinAddress,
3332
+ coin_name: coinName,
3333
+ coin_symbol: coinSymbol,
3334
+ amount_mode: amountMode,
3335
+ swap_amount_usd: swapAmountUsd,
3336
+ valueUsd: swapAmountUsd,
3337
+ swapCoinType,
3338
+ output_asset: outputAsset,
3339
+ slippage: slippagePct,
3340
+ output_format: output,
3341
+ success: false,
3342
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
3343
+ });
3344
+ await shutdownAnalytics();
3345
+ outputErrorAndExit(json, tradeErrorMessage(err));
3346
+ }
3347
+ txHash = receipt.transactionHash;
3348
+ if (outputToken.trade.type === "erc20") {
3349
+ try {
3350
+ const result = getReceivedAmountFromReceipt({
3351
+ receipt,
3352
+ tokenAddress: outputToken.trade.address,
3353
+ recipient: account.address
3354
+ });
3355
+ receivedAmountOut = result.amount;
3356
+ swapLogIndex = result.logIndex;
3357
+ receivedSource = "receipt";
3358
+ } catch {
3359
+ }
3360
+ }
3361
+ printSellResult(output, {
3362
+ coinName,
3363
+ coinSymbol,
3364
+ address: coinAddress,
3365
+ amountIn,
3366
+ coinDecimals,
3367
+ soldFormatted,
3368
+ receivedAmountOut,
3369
+ outputSymbol: outputToken.symbol,
3370
+ outputDecimals: outputToken.decimals,
3371
+ receivedSource,
3372
+ txHash
3373
+ });
3374
+ track("cli_sell", {
3375
+ action: "trade",
3376
+ coin_address: coinAddress,
3377
+ coin_name: coinName,
3378
+ coin_symbol: coinSymbol,
3379
+ amount_mode: amountMode,
3380
+ swap_amount_usd: swapAmountUsd,
3381
+ valueUsd: swapAmountUsd,
3382
+ swapCoinType,
3383
+ transactionHash: txHash,
3384
+ logIndex: swapLogIndex,
3385
+ output_asset: outputAsset,
3386
+ slippage: slippagePct,
3387
+ output_format: output,
3388
+ success: true,
3389
+ tx_hash: txHash
3390
+ });
3391
+ });
3392
+
3393
+ // src/commands/profile.tsx
3394
+ import { Command as Command8 } from "commander";
3395
+ import { Box as Box8, Text as Text8 } from "ink";
3396
+ import {
3397
+ getProfileCoins,
3398
+ getProfileBalances as getProfileBalances2,
3399
+ setApiKey as setApiKey7
3400
+ } from "@zoralabs/coins-sdk";
3401
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3402
+
3403
+ // src/components/ProfileView.tsx
3404
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
3405
+ import { Box as Box7, Text as Text7, useInput as useInput3, useApp as useApp3 } from "ink";
3406
+ import Spinner3 from "ink-spinner";
3407
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
3408
+ var TAB_NAMES = ["Posts", "Holdings"];
3409
+ var postColumns = [
3410
+ { header: "#", width: 4, accessor: (c) => String(c.rank) },
3411
+ { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
3412
+ {
3413
+ header: "Type",
3414
+ width: 14,
3415
+ accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
3416
+ },
3417
+ {
3418
+ header: "Market Cap",
3419
+ width: 12,
3420
+ accessor: (c) => formatCompactUsd(c.marketCap)
3421
+ },
3422
+ {
3423
+ header: "24h Vol",
3424
+ width: 12,
3425
+ accessor: (c) => formatCompactUsd(c.volume24h)
3426
+ },
3427
+ {
3428
+ header: "24h Change",
3429
+ width: 11,
3430
+ accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
3431
+ color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
3432
+ },
3433
+ {
3434
+ header: "Created",
3435
+ width: 16,
3436
+ accessor: (c) => {
3437
+ if (!c.createdAt) return "-";
3438
+ const date = new Date(c.createdAt);
3439
+ if (isNaN(date.getTime())) return "-";
3440
+ return formatRelativeTime(date);
3441
+ }
3442
+ }
3443
+ ];
3444
+ var ProfileView = ({
3445
+ fetchData,
3446
+ identifier,
3447
+ autoRefresh = false,
3448
+ intervalSeconds = 30
3449
+ }) => {
3450
+ const { exit } = useApp3();
3451
+ const [activeTab, setActiveTab] = useState4(0);
3452
+ const [loading, setLoading] = useState4(true);
3453
+ const [isRefreshing, setIsRefreshing] = useState4(false);
3454
+ const [error, setError] = useState4(null);
3455
+ const [data, setData] = useState4(null);
3456
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
3457
+ const [manualRefreshCount, setManualRefreshCount] = useState4(0);
3458
+ const hasLoadedOnce = useRef3(false);
3459
+ const load = useCallback4(async () => {
3460
+ if (hasLoadedOnce.current) {
3461
+ setIsRefreshing(true);
3462
+ } else {
3463
+ setLoading(true);
3464
+ }
3465
+ setError(null);
3466
+ try {
3467
+ const result = await fetchData();
3468
+ setData(result);
3469
+ hasLoadedOnce.current = true;
3470
+ } catch (err) {
3471
+ setError(err instanceof Error ? err.message : String(err));
3472
+ }
3473
+ setLoading(false);
3474
+ setIsRefreshing(false);
3475
+ }, [fetchData]);
3476
+ useEffect4(() => {
3477
+ load();
3478
+ }, [load, refreshCount, manualRefreshCount]);
3479
+ useInput3((input, key) => {
3480
+ if (input === "q" || key.escape) {
3481
+ exit();
3482
+ return;
3483
+ }
3484
+ if (input === "r" && !loading) {
3485
+ triggerManualRefresh();
3486
+ setManualRefreshCount((c) => c + 1);
3487
+ }
3488
+ if (key.leftArrow || input === "1") {
3489
+ setActiveTab(0);
3490
+ }
3491
+ if (key.rightArrow || input === "2") {
3492
+ setActiveTab(1);
3493
+ }
3494
+ });
3495
+ if (error && !data) {
3496
+ return /* @__PURE__ */ jsxs7(
3497
+ Box7,
3498
+ {
3499
+ flexDirection: "column",
3500
+ paddingLeft: 1,
3501
+ paddingTop: 1,
3502
+ paddingBottom: 1,
3503
+ children: [
3504
+ /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
3505
+ "Error: ",
3506
+ error
3507
+ ] }),
3508
+ /* @__PURE__ */ jsx10(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: "Press q to exit" }) })
3509
+ ]
3510
+ }
3511
+ );
3512
+ }
3513
+ if (loading && !data) {
3514
+ return /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
3515
+ /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
3516
+ " Loading profile\u2026"
3517
+ ] }) });
3518
+ }
3519
+ if (!data) return null;
3520
+ const hints = ["\u2190 \u2192 switch tab", "r refresh"];
3521
+ if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
3522
+ hints.push("q quit");
3523
+ const footer = hints.join(" \xB7 ");
3524
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
3525
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3526
+ isRefreshing && /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3527
+ /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
3528
+ " Refreshing\u2026"
3529
+ ] }) }),
3530
+ /* @__PURE__ */ jsxs7(Box7, { paddingLeft: 1, paddingTop: 1, gap: 2, children: [
3531
+ TAB_NAMES.map((name, i) => /* @__PURE__ */ jsx10(Text7, { bold: activeTab === i, dimColor: activeTab !== i, children: activeTab === i ? `[${name}]` : name }, name)),
3532
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3533
+ " ",
3534
+ identifier
3535
+ ] })
3536
+ ] }),
3537
+ activeTab === 0 ? rankedPosts.length === 0 ? /* @__PURE__ */ jsx10(
3538
+ Box7,
3539
+ {
3540
+ flexDirection: "column",
3541
+ paddingLeft: 1,
3542
+ paddingTop: 1,
3543
+ paddingBottom: 1,
3544
+ children: /* @__PURE__ */ jsx10(Text7, { children: "No posts found for this profile." })
3545
+ }
3546
+ ) : /* @__PURE__ */ jsx10(
3547
+ Table,
3548
+ {
3549
+ columns: postColumns,
3550
+ data: rankedPosts,
3551
+ title: "Posts",
3552
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
3553
+ }
3554
+ ) : data.holdings.length === 0 ? /* @__PURE__ */ jsx10(
3555
+ Box7,
3556
+ {
3557
+ flexDirection: "column",
3558
+ paddingLeft: 1,
3559
+ paddingTop: 1,
3560
+ paddingBottom: 1,
3561
+ children: /* @__PURE__ */ jsx10(Text7, { children: "No holdings found for this profile." })
3562
+ }
3563
+ ) : /* @__PURE__ */ jsx10(
3564
+ Table,
3565
+ {
3566
+ columns: balanceColumns,
3567
+ data: data.holdings,
3568
+ title: "Holdings",
3569
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
3570
+ }
3571
+ ),
3572
+ /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: footer }) })
3573
+ ] });
3574
+ };
3575
+
3576
+ // src/commands/profile.tsx
3577
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
3578
+ var extractErrorMessage2 = (error) => {
3579
+ if (typeof error === "object" && error !== null && "error" in error) {
3580
+ return String(error.error);
3581
+ }
3582
+ return JSON.stringify(error);
3583
+ };
3584
+ var resolveApiKey = (json) => {
3585
+ const apiKey = getApiKey();
3586
+ if (!apiKey) {
3587
+ outputErrorAndExit(
3588
+ json,
3589
+ "Not authenticated. Run 'zora auth configure' to set your API key."
3590
+ );
3591
+ }
3592
+ setApiKey7(apiKey);
3593
+ };
3594
+ var formatPostJson = (post, rank) => ({
3595
+ rank,
3596
+ name: post.name,
3597
+ symbol: post.symbol,
3598
+ coinType: COIN_TYPE_DISPLAY[post.coinType] ?? post.coinType,
3599
+ address: post.address,
3600
+ marketCap: post.marketCap ?? null,
3601
+ marketCapDelta24h: post.marketCapDelta24h ?? null,
3602
+ volume24h: post.volume24h ?? null,
3603
+ createdAt: post.createdAt ?? null
3604
+ });
3605
+ var formatHoldingJson = (balance) => {
3606
+ const priceUsd = balance.coin?.tokenPrice?.priceInUsdc ? Number(balance.coin.tokenPrice.priceInUsdc) : null;
3607
+ const usdValue = priceUsd !== null ? Number((parseRawBalance(balance.balance) * priceUsd).toFixed(6)) : null;
3608
+ return {
3609
+ rank: balance.rank,
3610
+ name: balance.coin?.name ?? null,
3611
+ symbol: balance.coin?.symbol ?? null,
3612
+ coinType: balance.coin?.coinType ?? null,
3613
+ address: balance.coin?.address ?? null,
3614
+ balance: normalizeTokenAmount(balance.balance),
3615
+ usdValue,
3616
+ priceUsd,
3617
+ marketCap: balance.coin?.marketCap ? Number(balance.coin.marketCap) : null
3618
+ };
3619
+ };
3620
+ var fetchProfileData = async (identifier) => {
3621
+ const [postsResult, holdingsResult] = await Promise.allSettled([
3622
+ getProfileCoins({ identifier, count: 20 }),
3623
+ getProfileBalances2({ identifier, count: 20, sortOption: "USD_VALUE" })
3624
+ ]);
3625
+ if (postsResult.status === "rejected") {
3626
+ throw new Error(
3627
+ postsResult.reason instanceof Error ? postsResult.reason.message : String(postsResult.reason)
3628
+ );
3629
+ }
3630
+ if (holdingsResult.status === "rejected") {
3631
+ throw new Error(
3632
+ holdingsResult.reason instanceof Error ? holdingsResult.reason.message : String(holdingsResult.reason)
3633
+ );
3634
+ }
3635
+ if (postsResult.value.error) {
3636
+ throw new Error(
3637
+ `API error (posts): ${extractErrorMessage2(postsResult.value.error)}`
3638
+ );
3639
+ }
3640
+ if (holdingsResult.value.error) {
3641
+ throw new Error(
3642
+ `API error (holdings): ${extractErrorMessage2(holdingsResult.value.error)}`
3643
+ );
3644
+ }
3645
+ const postEdges = postsResult.value.data?.profile?.createdCoins?.edges ?? [];
3646
+ const posts = postEdges.map((e) => e.node);
3647
+ const postsCount = postsResult.value.data?.profile?.createdCoins?.count ?? posts.length;
3648
+ const holdingEdges = holdingsResult.value.data?.profile?.coinBalances?.edges ?? [];
3649
+ const holdings = holdingEdges.map(
3650
+ (e, i) => ({
3651
+ ...e.node,
3652
+ rank: i + 1
3653
+ })
3654
+ );
3655
+ const holdingsCount = holdingsResult.value.data?.profile?.coinBalances?.count ?? holdings.length;
3656
+ return { posts, postsCount, holdings, holdingsCount };
3657
+ };
3658
+ var profileCommand = new Command8("profile").description("View profile activity (posts and holdings)").argument(
3659
+ "[identifier]",
3660
+ "Wallet address or profile handle (defaults to your wallet)"
3661
+ ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
3662
+ "--refresh <seconds>",
3663
+ "Auto-refresh interval in seconds, requires --live (min 5)",
3664
+ "30"
3665
+ ).action(async function(identifierArg) {
3666
+ const output = getOutputMode(this, "live");
3667
+ const json = output === "json";
3668
+ resolveApiKey(json);
3669
+ const { live, intervalSeconds } = getLiveConfig(this, output);
3670
+ let identifier = identifierArg;
3671
+ if (!identifier) {
3672
+ const envKey = process.env.ZORA_PRIVATE_KEY;
3673
+ const key = envKey || getPrivateKey();
3674
+ if (!key) {
3675
+ outputErrorAndExit(
3676
+ json,
3677
+ "No identifier provided and no wallet configured.",
3678
+ "Pass an address or handle, or run 'zora setup' first."
3679
+ );
3680
+ }
3681
+ try {
3682
+ identifier = privateKeyToAccount3(normalizeKey(key)).address;
3683
+ } catch {
3684
+ outputErrorAndExit(
3685
+ json,
3686
+ "Invalid wallet key. Run 'zora setup --force' to replace it."
3687
+ );
3688
+ }
3689
+ }
3690
+ if (json) {
3691
+ const data = await fetchProfileData(identifier).catch(
3692
+ (err) => outputErrorAndExit(
3693
+ json,
3694
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3695
+ )
3696
+ );
3697
+ outputData(json, {
3698
+ json: {
3699
+ posts: data.posts.map((p, i) => formatPostJson(p, i + 1)),
3700
+ holdings: data.holdings.map(formatHoldingJson)
3701
+ },
3702
+ render: () => {
3703
+ }
3704
+ });
3705
+ track("cli_profile", {
3706
+ identifier,
3707
+ output_format: "json",
3708
+ posts_count: data.postsCount,
3709
+ holdings_count: data.holdingsCount
3710
+ });
3711
+ } else if (live) {
3712
+ const fetchData = () => fetchProfileData(identifier);
3713
+ await renderLive(
3714
+ /* @__PURE__ */ jsx11(
3715
+ ProfileView,
3716
+ {
3717
+ fetchData,
3718
+ identifier,
3719
+ autoRefresh: live,
3720
+ intervalSeconds
3721
+ }
3722
+ )
3723
+ );
3724
+ track("cli_profile", {
3725
+ identifier,
3726
+ output_format: "live",
3727
+ live,
3728
+ interval: intervalSeconds
3729
+ });
3730
+ } else {
3731
+ const data = await fetchProfileData(identifier).catch(
3732
+ (err) => outputErrorAndExit(
3733
+ json,
3734
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3735
+ )
3736
+ );
3737
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
3738
+ renderOnce(
3739
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
3740
+ rankedPosts.length === 0 ? /* @__PURE__ */ jsx11(
3741
+ Box8,
3742
+ {
3743
+ flexDirection: "column",
3744
+ paddingLeft: 1,
3745
+ paddingTop: 1,
3746
+ paddingBottom: 1,
3747
+ children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No posts found for this profile." }) })
3748
+ }
3749
+ ) : /* @__PURE__ */ jsx11(
3750
+ Table,
3751
+ {
3752
+ columns: postColumns,
3753
+ data: rankedPosts,
3754
+ title: "Posts",
3755
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
3756
+ }
3757
+ ),
3758
+ data.holdings.length === 0 ? /* @__PURE__ */ jsx11(
3759
+ Box8,
3760
+ {
3761
+ flexDirection: "column",
3762
+ paddingLeft: 1,
3763
+ paddingTop: 1,
3764
+ paddingBottom: 1,
3765
+ children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No holdings found for this profile." }) })
3766
+ }
3767
+ ) : /* @__PURE__ */ jsx11(
3768
+ Table,
3769
+ {
3770
+ columns: balanceColumns,
3771
+ data: data.holdings,
3772
+ title: "Holdings",
3773
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
3774
+ }
3775
+ )
3776
+ ] })
3777
+ );
3778
+ track("cli_profile", {
3779
+ identifier,
3780
+ output_format: "static",
3781
+ posts_count: data.postsCount,
3782
+ holdings_count: data.holdingsCount
3783
+ });
3784
+ }
3785
+ });
3786
+
3787
+ // src/commands/send.ts
3788
+ import { Command as Command9 } from "commander";
3789
+ import confirm4 from "@inquirer/confirm";
3790
+ import {
3791
+ erc20Abi as erc20Abi4,
3792
+ formatUnits as formatUnits5,
3793
+ isAddress as isAddress3,
3794
+ parseUnits as parseUnits3
3795
+ } from "viem";
3796
+ import { setApiKey as setApiKey8 } from "@zoralabs/coins-sdk";
3797
+ var SEND_AMOUNT_CHECKS = {
3798
+ amount: (opts) => opts.amount !== void 0,
3799
+ percent: (opts) => opts.percent !== void 0,
3800
+ all: (opts) => opts.all === true
3801
+ };
3802
+ var VALID_TYPES3 = ["creator-coin", "post", "trend"];
3803
+ function printSendPreview(info) {
3804
+ const usdStr = info.amountUsd != null ? ` ($${info.amountUsd.toFixed(2)})` : "";
3805
+ console.log(`
3806
+ Send ${info.name} (${info.symbol})
3807
+ `);
3808
+ console.log(
3809
+ ` Amount ${info.amountFormatted} ${info.symbol}${usdStr}`
3810
+ );
3811
+ console.log(` To ${info.to}`);
3812
+ console.log(`
3813
+ Ensure receiving wallet can receive on Base.`);
3814
+ console.log("");
3815
+ }
3816
+ function printSendResult(json, info) {
3817
+ if (json) {
3818
+ outputJson({
3819
+ action: "send",
3820
+ coin: info.symbol,
3821
+ address: info.address,
3822
+ sent: {
3823
+ amount: formatUnits5(info.amount, info.decimals),
3824
+ raw: info.amount.toString(),
3825
+ symbol: info.symbol,
3826
+ amountUsd: info.amountUsd
3827
+ },
3828
+ to: info.to,
3829
+ tx: info.txHash
3830
+ });
3831
+ return;
3832
+ }
3833
+ const usdStr = info.amountUsd != null ? ` ($${info.amountUsd.toFixed(2)})` : "";
3834
+ console.log(`
3835
+ Sent ${info.name}
3836
+ `);
3837
+ console.log(
3838
+ ` Amount ${info.amountFormatted} ${info.symbol}${usdStr}`
3839
+ );
3840
+ console.log(` To ${info.to}`);
3841
+ console.log(` Tx ${info.txHash}
3842
+ `);
3843
+ }
3844
+ var sendCommand = new Command9("send").description("Send coins or ETH to an address").argument("[identifier]", "Coin address, name, or token (eth, usdc, zora)").option("--to <address>", "Recipient address (0x...)").option("--type <type>", "Coin type: creator-coin, post, trend").option("--amount <value>", "Send specific amount").option("--percent <value>", "Send percentage of balance (1-100)").option("--all", "Send entire balance").option("--yes", "Skip confirmation").action(async function(identifier, opts) {
3845
+ const json = getJson(this);
3846
+ if (!opts.to) {
3847
+ outputErrorAndExit(
3848
+ json,
3849
+ "Missing --to flag.",
3850
+ "Usage: zora send <identifier> --to <address>"
3851
+ );
3852
+ }
3853
+ if (!isAddress3(opts.to)) {
3854
+ outputErrorAndExit(
3855
+ json,
3856
+ `Invalid recipient address: ${opts.to}`,
3857
+ "Must be a valid 0x address."
3858
+ );
3859
+ }
3860
+ const recipient = opts.to;
3861
+ if (opts.type !== void 0 && !VALID_TYPES3.includes(opts.type)) {
3862
+ outputErrorAndExit(
3863
+ json,
3864
+ `Invalid --type value: ${opts.type}.`,
3865
+ `Supported: ${VALID_TYPES3.join(", ")}`
3866
+ );
3867
+ }
3868
+ const amountMode = getAmountMode(
3869
+ json,
3870
+ opts,
3871
+ SEND_AMOUNT_CHECKS,
3872
+ "--amount, --percent, or --all"
3873
+ );
3874
+ const isEth = identifier.toLowerCase() === "eth";
3875
+ if (isEth) {
3876
+ if (opts.type) {
3877
+ outputErrorAndExit(json, "--type is not valid when sending ETH.");
3878
+ }
3879
+ const account = resolveAccount(json);
3880
+ const { publicClient, walletClient } = createClients(account);
3881
+ const balance = await publicClient.getBalance({
3882
+ address: account.address
3883
+ });
3884
+ if (balance === 0n) {
3885
+ outputErrorAndExit(
3886
+ json,
3887
+ `No ETH balance. Deposit ETH to ${account.address} on Base.`
3888
+ );
3889
+ }
3890
+ let amount;
3891
+ if (amountMode === "amount") {
3892
+ const val = parsePercentageLikeValue(opts.amount);
3893
+ if (val === void 0 || val <= 0) {
3894
+ outputErrorAndExit(
3895
+ json,
3896
+ "Invalid --amount value. Must be a positive number."
3897
+ );
3898
+ }
3899
+ try {
3900
+ amount = parseUnits3(opts.amount, 18);
3901
+ } catch {
3902
+ outputErrorAndExit(
3903
+ json,
3904
+ "Invalid --amount value. Must be a valid ETH amount."
3905
+ );
3906
+ }
3907
+ if (amount === 0n) {
3908
+ outputErrorAndExit(
3909
+ json,
3910
+ "Amount too small \u2014 rounds to zero at 18 decimal places."
3911
+ );
3912
+ }
3913
+ if (amount + GAS_RESERVE > balance) {
3914
+ outputErrorAndExit(
3915
+ json,
3916
+ `Insufficient balance. Have ${formatEthDisplay(balance)} ETH (need to reserve ~${formatEthDisplay(GAS_RESERVE)} ETH for gas).`
3917
+ );
3918
+ }
3919
+ } else {
3920
+ if (balance <= GAS_RESERVE) {
3921
+ outputErrorAndExit(
3922
+ json,
3923
+ `Balance too low (${formatEthDisplay(balance)} ETH). Need >${formatEthDisplay(GAS_RESERVE)} ETH for gas.`
3924
+ );
3925
+ }
3926
+ const spendable = balance - GAS_RESERVE;
3927
+ if (amountMode === "all") {
3928
+ amount = spendable;
3929
+ } else {
3930
+ const pct = parsePercentageLikeValue(opts.percent);
3931
+ if (pct === void 0 || pct <= 0 || pct > 100) {
3932
+ outputErrorAndExit(
3933
+ json,
3934
+ "Invalid --percent value. Must be between 0 and 100."
3935
+ );
3936
+ }
3937
+ amount = pct === 100 ? spendable : spendable * BigInt(Math.round(pct * 100)) / 10000n;
3938
+ if (amount === 0n) {
3939
+ outputErrorAndExit(
3940
+ json,
3941
+ "Calculated amount is zero. Balance too low."
3942
+ );
3943
+ }
3944
+ }
3945
+ }
3946
+ const amountFormatted = formatEthDisplay(amount);
3947
+ let amountUsd = null;
3948
+ const ethPriceUsd = await fetchTokenPriceUsd(WETH_ADDRESS);
3949
+ if (ethPriceUsd != null) {
3950
+ amountUsd = Number(
3951
+ (Number(formatUnits5(amount, 18)) * ethPriceUsd).toFixed(2)
2208
3952
  );
2209
- return;
2210
3953
  }
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.");
3954
+ if (!opts.yes) {
3955
+ printSendPreview({
3956
+ name: "ETH",
3957
+ symbol: "ETH",
3958
+ amountFormatted,
3959
+ amountUsd,
3960
+ to: recipient
3961
+ });
3962
+ const ok = await confirm4({ message: "Confirm?", default: false });
3963
+ if (!ok) {
3964
+ console.error("Aborted.");
3965
+ process.exit(0);
3966
+ }
2215
3967
  }
2216
- if (debug) {
2217
- console.error(
2218
- `[debug] $${usdVal} USD = ${formatUnits4(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
3968
+ let txHash;
3969
+ try {
3970
+ txHash = await walletClient.sendTransaction({
3971
+ to: recipient,
3972
+ value: amount
3973
+ });
3974
+ } catch (err) {
3975
+ track("cli_send", {
3976
+ asset: "eth",
3977
+ output_format: json ? "json" : "static",
3978
+ success: false,
3979
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
3980
+ });
3981
+ await shutdownAnalytics();
3982
+ outputErrorAndExit(
3983
+ json,
3984
+ `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
2219
3985
  );
2220
3986
  }
2221
- } else if (amountMode === "amount") {
2222
- const val = parsePercentageLikeValue(opts.amount);
2223
- if (val === void 0 || val <= 0) {
3987
+ await publicClient.waitForTransactionReceipt({
3988
+ hash: txHash
3989
+ });
3990
+ printSendResult(json, {
3991
+ name: "ETH",
3992
+ symbol: "ETH",
3993
+ address: null,
3994
+ amount,
3995
+ decimals: 18,
3996
+ amountFormatted,
3997
+ amountUsd,
3998
+ to: recipient,
3999
+ txHash
4000
+ });
4001
+ track("cli_send", {
4002
+ asset: "eth",
4003
+ amount_mode: amountMode,
4004
+ amount_usd: amountUsd,
4005
+ transactionHash: txHash,
4006
+ output_format: json ? "json" : "static",
4007
+ success: true,
4008
+ tx_hash: txHash
4009
+ });
4010
+ } else {
4011
+ const knownTokenKey = identifier.toLowerCase();
4012
+ const knownToken = knownTokenKey !== "eth" && knownTokenKey in BASE_TRADE_TOKENS ? BASE_TRADE_TOKENS[knownTokenKey] : void 0;
4013
+ if (knownToken && opts.type) {
2224
4014
  outputErrorAndExit(
2225
4015
  json,
2226
- "Invalid --amount value. Must be a positive number."
4016
+ `--type is not valid when sending ${knownToken.symbol}.`
2227
4017
  );
2228
4018
  }
2229
- try {
2230
- amountIn = parseUnits2(opts.amount, coinDecimals);
2231
- } catch {
2232
- outputErrorAndExit(json, "Invalid --amount value for token decimals.");
4019
+ let tokenAddress;
4020
+ let tokenName;
4021
+ if (knownToken) {
4022
+ const trade = knownToken.trade;
4023
+ tokenAddress = trade.address;
4024
+ tokenName = knownToken.symbol;
4025
+ } else {
4026
+ const apiKey = getApiKey();
4027
+ if (apiKey) {
4028
+ setApiKey8(apiKey);
4029
+ }
4030
+ const ref = parseCoinRef(identifier, opts.type);
4031
+ let result;
4032
+ try {
4033
+ result = await resolveCoin(ref);
4034
+ } catch (err) {
4035
+ outputErrorAndExit(
4036
+ json,
4037
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
4038
+ );
4039
+ }
4040
+ if (result.kind === "not-found") {
4041
+ outputErrorAndExit(json, result.message, result.suggestion);
4042
+ }
4043
+ tokenAddress = result.coin.address;
4044
+ tokenName = result.coin.name;
4045
+ }
4046
+ const account = resolveAccount(json);
4047
+ const { publicClient, walletClient } = createClients(account);
4048
+ let balance;
4049
+ let decimals;
4050
+ let symbol;
4051
+ if (knownToken) {
4052
+ balance = await publicClient.readContract({
4053
+ abi: erc20Abi4,
4054
+ address: tokenAddress,
4055
+ functionName: "balanceOf",
4056
+ args: [account.address]
4057
+ });
4058
+ decimals = knownToken.decimals;
4059
+ symbol = knownToken.symbol;
4060
+ } else {
4061
+ const results = await Promise.all([
4062
+ publicClient.readContract({
4063
+ abi: erc20Abi4,
4064
+ address: tokenAddress,
4065
+ functionName: "balanceOf",
4066
+ args: [account.address]
4067
+ }),
4068
+ publicClient.readContract({
4069
+ abi: erc20Abi4,
4070
+ address: tokenAddress,
4071
+ functionName: "decimals"
4072
+ }),
4073
+ publicClient.readContract({
4074
+ abi: erc20Abi4,
4075
+ address: tokenAddress,
4076
+ functionName: "symbol"
4077
+ })
4078
+ ]);
4079
+ balance = results[0];
4080
+ decimals = results[1];
4081
+ symbol = results[2];
2233
4082
  }
2234
- } else {
2235
- const balance = await publicClient.readContract({
2236
- abi: erc20Abi3,
2237
- address: coinAddress,
2238
- functionName: "balanceOf",
2239
- args: [account.address]
2240
- });
2241
4083
  if (balance === 0n) {
2242
4084
  outputErrorAndExit(
2243
4085
  json,
2244
- `No ${coinSymbol} balance. Buy some first or pick a different wallet.`
4086
+ `No ${symbol} balance. Buy some first or pick a different wallet.`
2245
4087
  );
2246
4088
  }
2247
- if (amountMode === "all") {
2248
- amountIn = balance;
4089
+ let amount;
4090
+ if (amountMode === "amount") {
4091
+ const val = parsePercentageLikeValue(opts.amount);
4092
+ if (val === void 0 || val <= 0) {
4093
+ outputErrorAndExit(
4094
+ json,
4095
+ "Invalid --amount value. Must be a positive number."
4096
+ );
4097
+ }
4098
+ try {
4099
+ amount = parseUnits3(opts.amount, decimals);
4100
+ } catch {
4101
+ outputErrorAndExit(
4102
+ json,
4103
+ "Invalid --amount value for token decimals."
4104
+ );
4105
+ }
4106
+ if (amount === 0n) {
4107
+ outputErrorAndExit(
4108
+ json,
4109
+ `Amount too small \u2014 rounds to zero at ${decimals} decimal places.`
4110
+ );
4111
+ }
4112
+ if (amount > balance) {
4113
+ outputErrorAndExit(
4114
+ json,
4115
+ `Insufficient balance. Have ${formatAmountDisplay(balance, decimals)} ${symbol}.`
4116
+ );
4117
+ }
4118
+ } else if (amountMode === "all") {
4119
+ amount = balance;
2249
4120
  } else {
2250
4121
  const pct = parsePercentageLikeValue(opts.percent);
2251
4122
  if (pct === void 0 || pct <= 0 || pct > 100) {
@@ -2254,192 +4125,91 @@ var sellCommand = new Command6("sell").description("Sell a coin").argument("<add
2254
4125
  "Invalid --percent value. Must be between 0 and 100."
2255
4126
  );
2256
4127
  }
2257
- amountIn = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
2258
- if (amountIn === 0n) {
4128
+ amount = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
4129
+ if (amount === 0n) {
2259
4130
  outputErrorAndExit(
2260
4131
  json,
2261
4132
  "Calculated amount is zero. Balance too low."
2262
4133
  );
2263
4134
  }
2264
4135
  }
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
- `
4136
+ const amountFormatted = formatAmountDisplay(amount, decimals);
4137
+ let amountUsd = null;
4138
+ const priceAddress = knownToken ? knownToken.priceAddress : tokenAddress;
4139
+ const priceUsd = knownToken?.fixedPriceUsd ?? await fetchTokenPriceUsd(priceAddress);
4140
+ if (priceUsd != null) {
4141
+ amountUsd = Number(
4142
+ (Number(formatUnits5(amount, decimals)) * priceUsd).toFixed(2)
2296
4143
  );
2297
4144
  }
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);
4145
+ if (!opts.yes) {
4146
+ printSendPreview({
4147
+ name: tokenName,
4148
+ symbol,
4149
+ amountFormatted,
4150
+ amountUsd,
4151
+ to: recipient
4152
+ });
4153
+ const ok = await confirm4({ message: "Confirm?", default: false });
4154
+ if (!ok) {
4155
+ console.error("Aborted.");
4156
+ process.exit(0);
2305
4157
  }
4158
+ }
4159
+ let txHash;
4160
+ try {
4161
+ txHash = await walletClient.writeContract({
4162
+ abi: erc20Abi4,
4163
+ address: tokenAddress,
4164
+ functionName: "transfer",
4165
+ args: [recipient, amount]
4166
+ });
4167
+ } catch (err) {
4168
+ track("cli_send", {
4169
+ asset: knownToken ? knownTokenKey : "coin",
4170
+ coin_address: tokenAddress,
4171
+ coin_name: tokenName,
4172
+ coin_symbol: symbol,
4173
+ output_format: json ? "json" : "static",
4174
+ success: false,
4175
+ error_type: err instanceof Error ? err.constructor.name : "unknown"
4176
+ });
4177
+ await shutdownAnalytics();
2306
4178
  outputErrorAndExit(
2307
4179
  json,
2308
- "Not enough available liquidity for your swap. Please try swapping fewer tokens."
4180
+ `Transaction failed: ${err instanceof Error ? err.message : String(err)}`
2309
4181
  );
2310
4182
  }
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
4183
+ await publicClient.waitForTransactionReceipt({ hash: txHash });
4184
+ printSendResult(json, {
4185
+ name: tokenName,
4186
+ symbol,
4187
+ address: tokenAddress,
4188
+ amount,
4189
+ decimals,
4190
+ amountFormatted,
4191
+ amountUsd,
4192
+ to: recipient,
4193
+ txHash
2381
4194
  });
2382
- } catch (err) {
2383
- track("cli_sell", {
2384
- action: "trade",
2385
- coin_address: coinAddress,
2386
- coin_name: coinName,
2387
- coin_symbol: coinSymbol,
4195
+ track("cli_send", {
4196
+ asset: knownToken ? knownTokenKey : "coin",
4197
+ coin_address: tokenAddress,
4198
+ coin_name: tokenName,
4199
+ coin_symbol: symbol,
2388
4200
  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"
4201
+ amount_usd: amountUsd,
4202
+ transactionHash: txHash,
4203
+ output_format: json ? "json" : "static",
4204
+ success: true,
4205
+ tx_hash: txHash
2394
4206
  });
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
4207
  }
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
4208
  });
2439
4209
 
2440
4210
  // src/commands/setup.ts
2441
- import { Command as Command7 } from "commander";
2442
- import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
4211
+ import { Command as Command10 } from "commander";
4212
+ import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2443
4213
 
2444
4214
  // src/lib/strings.ts
2445
4215
  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";
@@ -2452,7 +4222,7 @@ var BACKUP_WARNING = "Back up this file \u2014 it's the only copy of your key.";
2452
4222
  var isValidPrivateKey = (key) => /^(0x)?[0-9a-fA-F]{64}$/.test(key);
2453
4223
  var toAccount = (json, key, errorPrefix) => {
2454
4224
  try {
2455
- return privateKeyToAccount3(normalizeKey(key));
4225
+ return privateKeyToAccount4(normalizeKey(key));
2456
4226
  } catch {
2457
4227
  outputErrorAndExit(
2458
4228
  json,
@@ -2460,7 +4230,7 @@ var toAccount = (json, key, errorPrefix) => {
2460
4230
  );
2461
4231
  }
2462
4232
  };
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) {
4233
+ var setupCommand = new Command10("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
4234
  const json = getJson(this);
2465
4235
  const nonInteractive = getYes(this);
2466
4236
  const envKey = process.env.ZORA_PRIVATE_KEY;
@@ -2475,7 +4245,7 @@ var setupCommand = new Command7("setup").description("Set up your Zora wallet").
2475
4245
  const account = toAccount(json, envKey, "ZORA_PRIVATE_KEY");
2476
4246
  outputData(json, {
2477
4247
  json: { source: "env", address: account.address },
2478
- table: () => {
4248
+ render: () => {
2479
4249
  console.log(" Using wallet from ZORA_PRIVATE_KEY.\n");
2480
4250
  console.log(` Address: ${account.address}
2481
4251
  `);
@@ -2496,14 +4266,14 @@ var setupCommand = new Command7("setup").description("Set up your Zora wallet").
2496
4266
  } catch (err) {
2497
4267
  outputErrorAndExit(
2498
4268
  json,
2499
- `\u2717 Could not read wallet: ${err.message}`,
4269
+ `\u2717 Could not read wallet: ${formatError(err)}`,
2500
4270
  "Run 'zora setup --force' to overwrite it."
2501
4271
  );
2502
4272
  }
2503
4273
  }
2504
4274
  if (existing) {
2505
4275
  const account = toAccount(json, existing, "Stored private key");
2506
- const truncated = `${account.address.slice(0, 6)}\u2026${account.address.slice(-4)}`;
4276
+ const truncated = truncateAddress(account.address);
2507
4277
  console.log(` Wallet already configured: ${truncated}
2508
4278
  `);
2509
4279
  if (!options.force) {
@@ -2565,7 +4335,7 @@ var setupCommand = new Command7("setup").description("Set up your Zora wallet").
2565
4335
  address: account.address,
2566
4336
  path: getWalletPath()
2567
4337
  },
2568
- table: () => {
4338
+ render: () => {
2569
4339
  console.log("\n\u2713 Wallet imported\n");
2570
4340
  console.log(` Address: ${account.address}`);
2571
4341
  console.log(` Private key: saved to ${getWalletPath()}
@@ -2600,7 +4370,7 @@ var setupCommand = new Command7("setup").description("Set up your Zora wallet").
2600
4370
  address: account.address,
2601
4371
  path: getWalletPath()
2602
4372
  },
2603
- table: () => {
4373
+ render: () => {
2604
4374
  console.log("\n\u2713 Wallet created\n");
2605
4375
  console.log(` Address: ${account.address}`);
2606
4376
  console.log(` Private key: saved to ${getWalletPath()}
@@ -2619,8 +4389,8 @@ var setupCommand = new Command7("setup").description("Set up your Zora wallet").
2619
4389
  });
2620
4390
 
2621
4391
  // src/commands/wallet.ts
2622
- import { Command as Command8 } from "commander";
2623
- import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
4392
+ import { Command as Command11 } from "commander";
4393
+ import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
2624
4394
  var resolvePrivateKey = () => {
2625
4395
  const envKey = process.env.ZORA_PRIVATE_KEY;
2626
4396
  if (envKey) {
@@ -2632,7 +4402,7 @@ var resolvePrivateKey = () => {
2632
4402
  }
2633
4403
  return void 0;
2634
4404
  };
2635
- var walletCommand = new Command8("wallet").description(
4405
+ var walletCommand = new Command11("wallet").description(
2636
4406
  "Manage your Zora wallet"
2637
4407
  );
2638
4408
  walletCommand.command("info").description("Show wallet address and storage location").action(function() {
@@ -2643,7 +4413,7 @@ walletCommand.command("info").description("Show wallet address and storage locat
2643
4413
  }
2644
4414
  let account;
2645
4415
  try {
2646
- account = privateKeyToAccount4(normalizeKey(resolved.key));
4416
+ account = privateKeyToAccount5(normalizeKey(resolved.key));
2647
4417
  } catch {
2648
4418
  const msg = resolved.source === "env" ? "ZORA_PRIVATE_KEY is not a valid private key." : "Stored private key is invalid.";
2649
4419
  const suggestion = resolved.source === "env" ? void 0 : "Run 'zora setup --force' to replace it.";
@@ -2652,7 +4422,7 @@ walletCommand.command("info").description("Show wallet address and storage locat
2652
4422
  const source = resolved.source === "env" ? "env (ZORA_PRIVATE_KEY)" : getWalletPath();
2653
4423
  outputData(json, {
2654
4424
  json: { address: account.address, source },
2655
- table: () => {
4425
+ render: () => {
2656
4426
  console.log(` Address: ${account.address}`);
2657
4427
  console.log(` Source: ${source}`);
2658
4428
  }
@@ -2692,7 +4462,7 @@ walletCommand.command("export").description("Print the raw private key to stdout
2692
4462
  });
2693
4463
 
2694
4464
  // src/components/Zorb.tsx
2695
- import { Text as Text4, Box as Box4 } from "ink";
4465
+ import { Text as Text9, Box as Box9 } from "ink";
2696
4466
 
2697
4467
  // src/lib/zorb-pixels.ts
2698
4468
  function supportsTruecolor() {
@@ -2837,7 +4607,7 @@ function generateZorbPixels(size) {
2837
4607
  }
2838
4608
 
2839
4609
  // src/components/Zorb.tsx
2840
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
4610
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
2841
4611
  var LOWER_HALF_BLOCK = "\u2584";
2842
4612
  var UPPER_HALF_BLOCK = "\u2580";
2843
4613
  function rgbString([r, g, b]) {
@@ -2860,19 +4630,19 @@ function Zorb({ size = 20 }) {
2860
4630
  const topIsBlack = isBlack(top);
2861
4631
  const bottomIsBlack = isBlack(bottom);
2862
4632
  if (topIsBlack && bottomIsBlack) {
2863
- cells.push(/* @__PURE__ */ jsx5(Text4, { children: " " }, x));
4633
+ cells.push(/* @__PURE__ */ jsx12(Text9, { children: " " }, x));
2864
4634
  } else if (topIsBlack) {
2865
4635
  cells.push(
2866
- /* @__PURE__ */ jsx5(Text4, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
4636
+ /* @__PURE__ */ jsx12(Text9, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
2867
4637
  );
2868
4638
  } else if (bottomIsBlack) {
2869
4639
  cells.push(
2870
- /* @__PURE__ */ jsx5(Text4, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
4640
+ /* @__PURE__ */ jsx12(Text9, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
2871
4641
  );
2872
4642
  } else {
2873
4643
  cells.push(
2874
- /* @__PURE__ */ jsx5(
2875
- Text4,
4644
+ /* @__PURE__ */ jsx12(
4645
+ Text9,
2876
4646
  {
2877
4647
  backgroundColor: rgbString(top),
2878
4648
  color: rgbString(bottom),
@@ -2883,41 +4653,54 @@ function Zorb({ size = 20 }) {
2883
4653
  );
2884
4654
  }
2885
4655
  }
2886
- rows.push(/* @__PURE__ */ jsx5(Text4, { children: cells }, y));
4656
+ rows.push(/* @__PURE__ */ jsx12(Text9, { children: cells }, y));
2887
4657
  }
2888
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2889
- /* @__PURE__ */ jsx5(Text4, { children: " " }),
4658
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
4659
+ /* @__PURE__ */ jsx12(Text9, { children: " " }),
2890
4660
  rows,
2891
- /* @__PURE__ */ jsx5(Text4, { children: " " })
4661
+ /* @__PURE__ */ jsx12(Text9, { children: " " })
2892
4662
  ] });
2893
4663
  }
2894
4664
 
2895
4665
  // src/index.tsx
2896
- import { jsx as jsx6 } from "react/jsx-runtime";
4666
+ import { jsx as jsx13 } from "react/jsx-runtime";
2897
4667
  if (process.env.ZORA_API_TARGET) {
2898
4668
  setApiBaseUrl(process.env.ZORA_API_TARGET);
2899
4669
  }
2900
- var version = true ? "0.2.3" : JSON.parse(
4670
+ var version = true ? "0.3.0" : JSON.parse(
2901
4671
  readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
2902
4672
  ).version;
2903
4673
  var buildProgram = () => {
2904
- const program2 = new Command9().name("zora").description("Zora CLI").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
4674
+ const program2 = new Command12().name("zora").description("Zora CLI").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
2905
4675
  program2.addCommand(authCommand);
2906
4676
  program2.addCommand(balanceCommand);
2907
4677
  program2.addCommand(buyCommand);
2908
4678
  program2.addCommand(exploreCommand);
2909
4679
  program2.addCommand(getCommand);
4680
+ program2.addCommand(priceHistoryCommand);
4681
+ program2.addCommand(profileCommand);
2910
4682
  program2.addCommand(setupCommand);
2911
4683
  program2.addCommand(walletCommand);
2912
4684
  program2.addCommand(sellCommand);
4685
+ program2.addCommand(sendCommand);
4686
+ program2.hook("preAction", (_thisCommand, actionCommand) => {
4687
+ const expected = actionCommand.registeredArguments.length;
4688
+ if (expected > 0 && actionCommand.args.length < expected) {
4689
+ actionCommand.outputHelp();
4690
+ process.exit(1);
4691
+ }
4692
+ });
2913
4693
  return program2;
2914
4694
  };
2915
4695
  var program = buildProgram();
2916
4696
  if (!process.env.VITEST) {
2917
4697
  const showingHelp = process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h");
2918
4698
  if (showingHelp && !process.argv.includes("--json") && supportsTruecolor()) {
2919
- renderOnce(/* @__PURE__ */ jsx6(Zorb, { size: 20 }));
4699
+ renderOnce(/* @__PURE__ */ jsx13(Zorb, { size: 20 }));
2920
4700
  }
4701
+ console.warn(
4702
+ "\x1B[33m\u26A0 Beta:\x1B[0m This CLI is in beta and should be used with caution."
4703
+ );
2921
4704
  identify();
2922
4705
  try {
2923
4706
  await program.parseAsync();