@zoralabs/cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
- import { Command as Command12 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
  import { ExitPromptError } from "@inquirer/core";
6
6
  import "fs";
7
7
  import { setApiBaseUrl } from "@zoralabs/coins-sdk";
@@ -20,11 +20,117 @@ import {
20
20
  import { join } from "path";
21
21
 
22
22
  // src/lib/errors.ts
23
- import { BaseError as ViemBaseError, InsufficientFundsError } from "viem";
23
+ import {
24
+ BaseError as ViemBaseError,
25
+ ContractFunctionRevertedError,
26
+ InsufficientFundsError,
27
+ decodeErrorResult
28
+ } from "viem";
29
+ var TRADE_ERROR_ABI = [
30
+ { type: "error", name: "SlippageBoundsExceeded", inputs: [] },
31
+ { type: "error", name: "InsufficientLiquidity", inputs: [] },
32
+ { type: "error", name: "InsufficientFunds", inputs: [] },
33
+ { type: "error", name: "EthAmountTooSmall", inputs: [] },
34
+ { type: "error", name: "EthAmountMismatch", inputs: [] },
35
+ { type: "error", name: "ERC20TransferAmountMismatch", inputs: [] },
36
+ { type: "error", name: "EthTransferFailed", inputs: [] },
37
+ { type: "error", name: "EthTransferInvalid", inputs: [] },
38
+ { type: "error", name: "MarketNotGraduated", inputs: [] },
39
+ { type: "error", name: "MarketAlreadyGraduated", inputs: [] },
40
+ { type: "error", name: "NotEnoughLiquidity", inputs: [] },
41
+ { type: "error", name: "InsufficientOutputAmount", inputs: [] },
42
+ { type: "error", name: "InvalidPrice", inputs: [] },
43
+ { type: "error", name: "InvalidPriceOrLiquidity", inputs: [] },
44
+ { type: "error", name: "PriceOverflow", inputs: [] },
45
+ {
46
+ type: "error",
47
+ name: "SwapReverted",
48
+ inputs: [{ name: "error", type: "bytes" }]
49
+ },
50
+ {
51
+ type: "error",
52
+ name: "OnlyPool",
53
+ inputs: [
54
+ { name: "sender", type: "address" },
55
+ { name: "pool", type: "address" }
56
+ ]
57
+ },
58
+ { type: "error", name: "OnlyWeth", inputs: [] }
59
+ ];
60
+ var TRADE_ERROR_MESSAGES = {
61
+ // Slippage / price
62
+ SlippageBoundsExceeded: "Price moved too much during your trade. Try increasing --slippage (e.g. --slippage 3) or reducing the amount.",
63
+ InsufficientOutputAmount: "Trade would produce less output than the minimum. Try increasing --slippage or reducing the amount.",
64
+ InvalidPrice: "Invalid price calculation. The pool may be in an unusual state. Try again later.",
65
+ InvalidPriceOrLiquidity: "Invalid price or liquidity state. The pool may be in an unusual state. Try again later.",
66
+ PriceOverflow: "Price calculation overflow. Try a smaller trade amount.",
67
+ // Liquidity
68
+ InsufficientLiquidity: "Not enough liquidity in the pool for this trade. Try a smaller amount.",
69
+ NotEnoughLiquidity: "Not enough liquidity in the pool for this trade. Try a smaller amount.",
70
+ InsufficientFunds: "Not enough funds. Try a lower amount or run 'zora balance spendable' to check your balance.",
71
+ // Transfer
72
+ EthAmountTooSmall: "ETH amount is too small to execute this trade. Try a larger amount.",
73
+ EthAmountMismatch: "ETH amount sent doesn't match the expected value. Please report this issue.",
74
+ ERC20TransferAmountMismatch: "Token transfer amount mismatch. The token may have a transfer fee. Try a different amount.",
75
+ EthTransferFailed: "ETH transfer failed. The recipient may not accept ETH.",
76
+ EthTransferInvalid: "Invalid ETH transfer. This trade uses a token pair that doesn't involve ETH.",
77
+ SwapReverted: "The underlying swap failed. Try a different amount or increasing --slippage.",
78
+ // Market state
79
+ MarketNotGraduated: "This coin's market hasn't graduated yet. It may not support this trade type.",
80
+ MarketAlreadyGraduated: "This coin's market has already graduated. Try trading through the graduated pool.",
81
+ // Access (internal routing — unlikely to reach users)
82
+ OnlyPool: "This function can only be called by the pool contract.",
83
+ OnlyWeth: "This function only accepts WETH."
84
+ };
85
+ function findHexData(value) {
86
+ if (typeof value === "string" && value.startsWith("0x") && value.length >= 10) {
87
+ return value;
88
+ }
89
+ if (value && typeof value === "object" && "data" in value) {
90
+ return findHexData(value.data);
91
+ }
92
+ return void 0;
93
+ }
94
+ function extractRevertData(err) {
95
+ let current = err;
96
+ while (current && typeof current === "object") {
97
+ const data = findHexData(current.data);
98
+ if (data) return data;
99
+ current = current.cause;
100
+ }
101
+ return void 0;
102
+ }
103
+ function decodeTradeRevert(err) {
104
+ const revertError = err.walk(
105
+ (e) => e instanceof ContractFunctionRevertedError
106
+ );
107
+ if (revertError instanceof ContractFunctionRevertedError) {
108
+ const errorName = revertError.data?.errorName;
109
+ if (errorName && errorName !== "Error" && errorName !== "Panic") {
110
+ return TRADE_ERROR_MESSAGES[errorName] ?? `Transaction reverted: ${errorName}`;
111
+ }
112
+ if (revertError.reason) {
113
+ return `Transaction reverted: ${revertError.reason}`;
114
+ }
115
+ }
116
+ const revertData = extractRevertData(err);
117
+ if (revertData) {
118
+ try {
119
+ const decoded = decodeErrorResult({
120
+ abi: TRADE_ERROR_ABI,
121
+ data: revertData
122
+ });
123
+ return TRADE_ERROR_MESSAGES[decoded.errorName] ?? `Transaction reverted: ${decoded.errorName}`;
124
+ } catch {
125
+ }
126
+ }
127
+ return void 0;
128
+ }
129
+ var MAX_ERROR_LENGTH = 120;
24
130
  function formatError(err) {
25
131
  if (!(err instanceof Error)) return String(err);
26
132
  const msg = err.message;
27
- return msg.length > 120 ? msg.slice(0, 120) + "..." : msg;
133
+ return msg.length > MAX_ERROR_LENGTH ? msg.slice(0, MAX_ERROR_LENGTH) + "..." : msg;
28
134
  }
29
135
  function tradeErrorMessage(err) {
30
136
  if (!(err instanceof Error)) return String(err);
@@ -32,6 +138,8 @@ function tradeErrorMessage(err) {
32
138
  const insufficient = err.walk((e) => e instanceof InsufficientFundsError);
33
139
  if (insufficient)
34
140
  return "Not enough funds. Try a lower amount or run 'zora balance spendable' to check your balance.";
141
+ const decoded = decodeTradeRevert(err);
142
+ if (decoded) return decoded;
35
143
  return err.shortMessage;
36
144
  }
37
145
  return apiErrorMessage(err);
@@ -52,6 +160,12 @@ function apiErrorMessage(err) {
52
160
  return "Zora is temporarily unavailable. Try again later.";
53
161
  return formatError(err);
54
162
  }
163
+ function extractErrorMessage(error) {
164
+ if (typeof error === "object" && error !== null && "error" in error) {
165
+ return String(error.error);
166
+ }
167
+ return JSON.stringify(error);
168
+ }
55
169
  function bannedCoinMessage(address) {
56
170
  return `The coin at ${address} is unavailable because it violates the Zora terms of service.`;
57
171
  }
@@ -67,6 +181,21 @@ function fsErrorMessage(err, path) {
67
181
  return formatError(err);
68
182
  }
69
183
 
184
+ // src/lib/exit.ts
185
+ var SUCCESS = 0;
186
+ var ERROR = 1;
187
+ var CliExitError = class extends Error {
188
+ exitCode;
189
+ constructor(code) {
190
+ super(`process.exit(${code})`);
191
+ this.name = "CliExitError";
192
+ this.exitCode = code;
193
+ }
194
+ };
195
+ var safeExit = (code) => {
196
+ throw new CliExitError(code);
197
+ };
198
+
70
199
  // src/lib/config.ts
71
200
  import { homedir, platform } from "os";
72
201
  function getConfigDir() {
@@ -164,7 +293,7 @@ function getEnvApiKey() {
164
293
  console.error(
165
294
  "ZORA_API_KEY is set but empty. Provide a valid key or unset the variable."
166
295
  );
167
- process.exit(1);
296
+ safeExit(ERROR);
168
297
  }
169
298
  return envKey;
170
299
  }
@@ -243,7 +372,7 @@ var outputErrorAndExit = (json, message, suggestion) => {
243
372
  console.error(`\x1B[2m${suggestion}\x1B[0m`);
244
373
  }
245
374
  }
246
- process.exit(1);
375
+ safeExit(ERROR);
247
376
  };
248
377
  var outputData = (json, opts) => {
249
378
  if (json) {
@@ -346,14 +475,14 @@ var resolveAccount = (json = false) => {
346
475
  console.error(
347
476
  "No wallet configured. Run 'zora setup' to create or import one."
348
477
  );
349
- return process.exit(1);
478
+ safeExit(ERROR);
350
479
  }
351
480
  try {
352
481
  return privateKeyToAccount(normalizeKey(key));
353
482
  } catch (err) {
354
483
  console.error(`\u2717 Invalid private key: ${formatError(err)}`);
355
484
  console.error(" Run 'zora setup --force' to replace it.");
356
- return process.exit(1);
485
+ safeExit(ERROR);
357
486
  }
358
487
  };
359
488
  function formatRpcError(error) {
@@ -365,6 +494,27 @@ function formatRpcError(error) {
365
494
  }
366
495
  return JSON.stringify(error);
367
496
  }
497
+ function extractRpcHexData(data) {
498
+ if (typeof data === "string" && data.startsWith("0x") && data.length >= 10) {
499
+ return data;
500
+ }
501
+ if (data && typeof data === "object" && "data" in data) {
502
+ return extractRpcHexData(data.data);
503
+ }
504
+ return void 0;
505
+ }
506
+ function buildRpcError(rpcError) {
507
+ if (rpcError && typeof rpcError === "object" && "code" in rpcError && typeof rpcError.code === "number") {
508
+ const typed = rpcError;
509
+ const err = new Error(typed.message ?? "RPC error");
510
+ err.code = typed.code;
511
+ if (typed.data !== void 0) {
512
+ err.data = extractRpcHexData(typed.data) ?? typed.data;
513
+ }
514
+ return err;
515
+ }
516
+ return new Error(`CLI RPC request failed: ${formatRpcError(rpcError)}`);
517
+ }
368
518
  function createCliRpcTransport(chainId = base.id) {
369
519
  return custom({
370
520
  async request({
@@ -382,15 +532,11 @@ function createCliRpcTransport(chainId = base.id) {
382
532
  throw new Error(`CLI RPC request failed: ${formatRpcError(err)}`);
383
533
  }
384
534
  if (response.error) {
385
- throw new Error(
386
- `CLI RPC request failed: ${formatRpcError(response.error)}`
387
- );
535
+ throw buildRpcError(response.error);
388
536
  }
389
537
  const payload = response.data;
390
538
  if (payload && typeof payload === "object" && "error" in payload && payload.error) {
391
- throw new Error(
392
- `CLI RPC request failed: ${formatRpcError(payload.error)}`
393
- );
539
+ throw buildRpcError(payload.error);
394
540
  }
395
541
  if (payload && typeof payload === "object" && "result" in payload) {
396
542
  return payload.result;
@@ -441,7 +587,7 @@ var getClient = () => {
441
587
  return client;
442
588
  };
443
589
  var commonProperties = () => ({
444
- cli_version: true ? "1.0.0" : "development",
590
+ cli_version: true ? "1.1.0" : "development",
445
591
  os: process.platform,
446
592
  arch: process.arch,
447
593
  node_version: process.version
@@ -595,7 +741,7 @@ authCommand.command("status").description("Check authentication status").action(
595
741
 
596
742
  // src/commands/balance.tsx
597
743
  import { Command as Command2 } from "commander";
598
- import { Box as Box3, Text as Text3 } from "ink";
744
+ import { Box as Box5, Text as Text5 } from "ink";
599
745
  import { getProfileBalances, setApiKey } from "@zoralabs/coins-sdk";
600
746
 
601
747
  // src/lib/render.tsx
@@ -684,6 +830,28 @@ var Table = ({
684
830
  // src/lib/format.ts
685
831
  import { format, formatDistanceStrict } from "date-fns";
686
832
  import { formatUnits } from "viem";
833
+
834
+ // src/lib/types.ts
835
+ var SORT_LABELS = {
836
+ mcap: "Top by Market Cap",
837
+ volume: "Top by 24h Volume",
838
+ new: "New",
839
+ trending: "Trending",
840
+ featured: "Featured"
841
+ };
842
+ var TYPE_LABELS = {
843
+ all: "all",
844
+ trend: "trends",
845
+ "creator-coin": "creator coins",
846
+ post: "posts"
847
+ };
848
+ var COIN_TYPE_DISPLAY = {
849
+ CONTENT: "post",
850
+ CREATOR: "creator-coin",
851
+ TREND: "trend"
852
+ };
853
+
854
+ // src/lib/format.ts
687
855
  function formatCompactUsd(value) {
688
856
  if (!value || Number(value) === 0) return "$0";
689
857
  return new Intl.NumberFormat("en-US", {
@@ -753,6 +921,9 @@ var formatAmountDisplay = (amount, decimals) => {
753
921
  maximumFractionDigits: 2
754
922
  }).format(Number(formatted));
755
923
  };
924
+ var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
925
+ maximumFractionDigits: 2
926
+ }).format(Number(coinsOut));
756
927
  function computeMarketCapChange24h(marketCap, marketCapDelta24h) {
757
928
  if (marketCap === null || marketCapDelta24h === null || marketCap - marketCapDelta24h === 0)
758
929
  return null;
@@ -761,6 +932,17 @@ function computeMarketCapChange24h(marketCap, marketCapDelta24h) {
761
932
  );
762
933
  }
763
934
  var truncateAddress = (address) => `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
935
+ function formatCoinType(coinType) {
936
+ if (!coinType) return "";
937
+ return COIN_TYPE_DISPLAY[coinType] ?? coinType;
938
+ }
939
+ function formatCoinName(coin) {
940
+ if (!coin) return "Unknown";
941
+ if (coin.coinType === "CONTENT" && !coin.name && coin.address) {
942
+ return truncateAddress(coin.address);
943
+ }
944
+ return coin.name ?? "Unknown";
945
+ }
764
946
 
765
947
  // src/lib/balance-format.ts
766
948
  var COIN_DECIMALS = 18;
@@ -779,11 +961,17 @@ var normalizeTokenAmount = (rawBalance, decimals = COIN_DECIMALS) => {
779
961
  return rawBalance;
780
962
  }
781
963
  };
782
- var formatBalanceAsUsd = (balance, priceInUsdc) => {
783
- if (!priceInUsdc) return "-";
784
- const value = parseRawBalance(balance) * Number(priceInUsdc);
785
- if (value < 0.01) return "<$0.01";
786
- return formatUsd(value);
964
+ var computeBalanceUsdValue = (balance, marketValueUsd, priceInUsdc) => {
965
+ if (marketValueUsd != null && marketValueUsd !== "") {
966
+ const parsed = Number(marketValueUsd);
967
+ if (!Number.isFinite(parsed)) return null;
968
+ return Number(parsed.toFixed(6));
969
+ }
970
+ if (!priceInUsdc) return null;
971
+ const price = Number(priceInUsdc);
972
+ if (!Number.isFinite(price)) return null;
973
+ const value = parseRawBalance(balance) * price;
974
+ return Number(value.toFixed(6));
787
975
  };
788
976
  var formatBalance = (balance) => {
789
977
  const n = parseRawBalance(balance);
@@ -803,7 +991,7 @@ var trimTrailingZeros = (value) => {
803
991
  };
804
992
 
805
993
  // src/lib/balance-columns.tsx
806
- var SORT_LABELS = {
994
+ var SORT_LABELS2 = {
807
995
  "usd-value": "USD Value",
808
996
  balance: "Balance",
809
997
  "market-cap": "Market Cap",
@@ -824,14 +1012,18 @@ var balanceColumns = [
824
1012
  { header: "#", width: 5, accessor: (row) => String(row.rank) },
825
1013
  {
826
1014
  header: "Name",
827
- width: 24,
828
- accessor: (row) => row.coin?.name ?? "Unknown"
1015
+ width: 20,
1016
+ accessor: (row) => formatCoinName(row.coin)
829
1017
  },
830
1018
  {
831
- header: "Symbol",
832
- width: 12,
833
- noTruncate: true,
834
- accessor: (row) => row.coin?.symbol ?? ""
1019
+ header: "Address",
1020
+ width: 14,
1021
+ accessor: (row) => row.coin?.address ? truncateAddress(row.coin.address) : ""
1022
+ },
1023
+ {
1024
+ header: "Type",
1025
+ width: 14,
1026
+ accessor: (row) => formatCoinType(row.coin?.coinType)
835
1027
  },
836
1028
  {
837
1029
  header: "Balance",
@@ -841,7 +1033,16 @@ var balanceColumns = [
841
1033
  {
842
1034
  header: "USD Value",
843
1035
  width: 14,
844
- accessor: (row) => formatBalanceAsUsd(row.balance, row.coin?.tokenPrice?.priceInUsdc)
1036
+ accessor: (row) => {
1037
+ const value = computeBalanceUsdValue(
1038
+ row.balance,
1039
+ row.valuation?.marketValueUsd,
1040
+ row.coin?.tokenPrice?.priceInUsdc
1041
+ );
1042
+ if (value === null) return "-";
1043
+ if (value < 0.01) return "<$0.01";
1044
+ return formatUsd(value);
1045
+ }
845
1046
  },
846
1047
  {
847
1048
  header: "Market Cap",
@@ -855,6 +1056,7 @@ var balanceColumns = [
855
1056
  color: (row) => formatMcapChange(row.coin?.marketCap, row.coin?.marketCapDelta24h).color
856
1057
  }
857
1058
  ];
1059
+ var API_KEY_BANNER = "Valuation is more accurately evaluated when using an API key. Run `zora setup` to configure one.";
858
1060
 
859
1061
  // src/hooks/use-auto-refresh.ts
860
1062
  import { useState, useEffect, useCallback } from "react";
@@ -863,10 +1065,11 @@ var useAutoRefresh = (intervalSeconds, enabled) => {
863
1065
  const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(intervalSeconds);
864
1066
  const [resetCount, setResetCount] = useState(0);
865
1067
  const triggerManualRefresh = useCallback(() => {
866
- if (!enabled) return;
867
1068
  setRefreshCount((c) => c + 1);
868
- setSecondsUntilRefresh(intervalSeconds);
869
- setResetCount((c) => c + 1);
1069
+ if (enabled) {
1070
+ setSecondsUntilRefresh(intervalSeconds);
1071
+ setResetCount((c) => c + 1);
1072
+ }
870
1073
  }, [enabled, intervalSeconds]);
871
1074
  useEffect(() => {
872
1075
  if (!enabled) return;
@@ -882,10 +1085,41 @@ var useAutoRefresh = (intervalSeconds, enabled) => {
882
1085
  }, 1e3);
883
1086
  return () => clearInterval(ticker);
884
1087
  }, [enabled, intervalSeconds, resetCount]);
885
- if (!enabled) {
886
- return { refreshCount: 0, secondsUntilRefresh: 0, triggerManualRefresh };
1088
+ return {
1089
+ refreshCount,
1090
+ secondsUntilRefresh: enabled ? secondsUntilRefresh : 0,
1091
+ triggerManualRefresh
1092
+ };
1093
+ };
1094
+
1095
+ // src/lib/clipboard.ts
1096
+ import { execFileSync } from "child_process";
1097
+ import { platform as platform2 } from "os";
1098
+ var copyToClipboard = (text) => {
1099
+ const os = platform2();
1100
+ try {
1101
+ if (os === "darwin") {
1102
+ execFileSync("pbcopy", {
1103
+ input: text,
1104
+ stdio: ["pipe", "ignore", "ignore"]
1105
+ });
1106
+ } else if (os === "linux") {
1107
+ execFileSync("xclip", ["-selection", "clipboard"], {
1108
+ input: text,
1109
+ stdio: ["pipe", "ignore", "ignore"]
1110
+ });
1111
+ } else if (os === "win32") {
1112
+ execFileSync("clip", {
1113
+ input: text,
1114
+ stdio: ["pipe", "ignore", "ignore"]
1115
+ });
1116
+ } else {
1117
+ return false;
1118
+ }
1119
+ return true;
1120
+ } catch {
1121
+ return false;
887
1122
  }
888
- return { refreshCount, secondsUntilRefresh, triggerManualRefresh };
889
1123
  };
890
1124
 
891
1125
  // src/components/BalanceView.tsx
@@ -894,74 +1128,76 @@ var BalanceView = ({
894
1128
  fetchData,
895
1129
  sort,
896
1130
  mode = "full",
897
- initialCursor,
898
1131
  autoRefresh = false,
899
- intervalSeconds = 30
1132
+ intervalSeconds = 30,
1133
+ hasApiKey = false
900
1134
  }) => {
901
1135
  const { exit } = useApp();
902
1136
  const [loading, setLoading] = useState2(true);
903
1137
  const [isRefreshing, setIsRefreshing] = useState2(false);
904
1138
  const [error, setError] = useState2(null);
905
1139
  const [data, setData] = useState2(null);
906
- const paginated = mode === "coins";
907
- const [page, setPage] = useState2(1);
908
- const [cursorHistory, setCursorHistory] = useState2(
909
- []
910
- );
911
- const [currentCursor, setCurrentCursor] = useState2(
912
- initialCursor
913
- );
914
1140
  const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
915
- const [manualRefreshCount, setManualRefreshCount] = useState2(0);
916
1141
  const hasLoadedOnce = useRef(false);
917
- const load = useCallback2(
918
- async (cursor) => {
919
- if (hasLoadedOnce.current) {
920
- setIsRefreshing(true);
921
- } else {
922
- setLoading(true);
923
- }
924
- setError(null);
925
- try {
926
- const result = await fetchData(cursor);
927
- setData(result);
928
- hasLoadedOnce.current = true;
929
- } catch (err) {
930
- setError(err instanceof Error ? err.message : String(err));
931
- }
932
- setLoading(false);
933
- setIsRefreshing(false);
934
- },
935
- [fetchData]
936
- );
1142
+ const [selectedRow, setSelectedRow] = useState2(0);
1143
+ const [copyFeedback, setCopyFeedback] = useState2(null);
1144
+ const load = useCallback2(async () => {
1145
+ if (hasLoadedOnce.current) {
1146
+ setIsRefreshing(true);
1147
+ } else {
1148
+ setLoading(true);
1149
+ }
1150
+ setError(null);
1151
+ try {
1152
+ const result = await fetchData();
1153
+ setData(result);
1154
+ hasLoadedOnce.current = true;
1155
+ } catch (err) {
1156
+ setError(err instanceof Error ? err.message : String(err));
1157
+ }
1158
+ setLoading(false);
1159
+ setIsRefreshing(false);
1160
+ }, [fetchData]);
1161
+ useEffect2(() => {
1162
+ load();
1163
+ }, [load, refreshCount]);
1164
+ const showCoins = mode === "full";
937
1165
  useEffect2(() => {
938
- load(currentCursor);
939
- }, [load, refreshCount, manualRefreshCount, currentCursor]);
1166
+ if (data && showCoins) {
1167
+ setSelectedRow(
1168
+ (r) => Math.min(r, Math.max(0, data.rankedBalances.length - 1))
1169
+ );
1170
+ }
1171
+ }, [data, showCoins]);
940
1172
  useInput((input, key) => {
941
1173
  if (input === "q" || key.escape) {
942
1174
  exit();
943
1175
  return;
944
1176
  }
945
1177
  if (loading) return;
1178
+ if (showCoins && data && data.rankedBalances.length > 0) {
1179
+ if (key.upArrow || input === "k") {
1180
+ setSelectedRow((r) => Math.max(0, r - 1));
1181
+ return;
1182
+ }
1183
+ if (key.downArrow || input === "j") {
1184
+ setSelectedRow((r) => Math.min(data.rankedBalances.length - 1, r + 1));
1185
+ return;
1186
+ }
1187
+ if (input === "c" || key.return) {
1188
+ const coin = data.rankedBalances[selectedRow]?.coin;
1189
+ if (coin?.address) {
1190
+ const ok = copyToClipboard(coin.address);
1191
+ setCopyFeedback(ok ? "Copied!" : "Copy failed");
1192
+ setTimeout(() => setCopyFeedback(null), 1500);
1193
+ }
1194
+ return;
1195
+ }
1196
+ }
946
1197
  if (input === "r") {
947
1198
  triggerManualRefresh();
948
- setManualRefreshCount((c) => c + 1);
949
1199
  return;
950
1200
  }
951
- if (!paginated) return;
952
- const canGoNext = data?.pageInfo?.hasNextPage && data.pageInfo.endCursor;
953
- const canGoPrev = cursorHistory.length > 0;
954
- if ((input === "n" || key.rightArrow) && canGoNext) {
955
- setCursorHistory((prev) => [...prev, currentCursor]);
956
- setCurrentCursor(data.pageInfo.endCursor);
957
- setPage((p) => p + 1);
958
- }
959
- if ((input === "p" || key.leftArrow) && canGoPrev) {
960
- const prev = cursorHistory[cursorHistory.length - 1];
961
- setCursorHistory((h) => h.slice(0, -1));
962
- setCurrentCursor(prev);
963
- setPage((p) => p - 1);
964
- }
965
1201
  });
966
1202
  if (error && !data) {
967
1203
  return /* @__PURE__ */ jsxs2(
@@ -989,13 +1225,14 @@ var BalanceView = ({
989
1225
  }
990
1226
  if (!data) return null;
991
1227
  const hints = [];
992
- if (paginated && cursorHistory.length > 0) hints.push("\u2190 prev");
993
- if (paginated && data?.pageInfo?.hasNextPage) hints.push("\u2192 next");
1228
+ if (showCoins && data.rankedBalances.length > 0) {
1229
+ hints.push("\u2191\u2193 select");
1230
+ hints.push("enter/c copy address");
1231
+ }
994
1232
  hints.push(autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh");
995
1233
  hints.push("q quit");
996
- const footer = hints.join(" \xB7 ");
1234
+ const footer = hints.join(" \xB7 ") + (copyFeedback ? ` ${copyFeedback}` : "");
997
1235
  const showWallet = mode === "full" || mode === "wallet";
998
- const showCoins = mode === "full" || mode === "coins";
999
1236
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1000
1237
  isRefreshing && /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1001
1238
  /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
@@ -1033,94 +1270,328 @@ var BalanceView = ({
1033
1270
  {
1034
1271
  columns: balanceColumns,
1035
1272
  data: data.rankedBalances,
1036
- title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1037
- subtitle: paginated ? `Page ${page} \xB7 ${data.rankedBalances.length} result${data.rankedBalances.length !== 1 ? "s" : ""}` : `${data.rankedBalances.length} of ${data.total}`
1273
+ title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
1274
+ subtitle: `${data.rankedBalances.length} of ${data.total}`,
1275
+ selectedRow
1038
1276
  }
1039
1277
  ) : null,
1278
+ !hasApiKey && showCoins && data.rankedBalances.length > 0 && /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: API_KEY_BANNER }) }),
1040
1279
  /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: footer }) })
1041
1280
  ] });
1042
1281
  };
1043
1282
 
1044
- // src/lib/wallet-balances.ts
1045
- import { getTokenInfo } from "@zoralabs/coins-sdk";
1283
+ // src/components/BalanceCoinsView.tsx
1284
+ import { Box as Box4, Text as Text4 } from "ink";
1285
+
1286
+ // src/components/PaginatedTableView.tsx
1046
1287
  import {
1047
- createPublicClient as createPublicClient2,
1048
- erc20Abi,
1049
- formatUnits as formatUnits2,
1050
- http
1051
- } from "viem";
1052
- import { base as base2 } from "viem/chains";
1053
- var TRACKED_TOKENS = [
1054
- {
1055
- name: "Ether",
1056
- symbol: "ETH",
1057
- address: WETH_ADDRESS,
1058
- decimals: 18,
1059
- priceAddress: WETH_ADDRESS,
1060
- isNative: true
1061
- },
1062
- {
1063
- name: "USD Coin",
1064
- symbol: "USDC",
1065
- address: USDC_ADDRESS,
1066
- decimals: USDC_DECIMALS,
1067
- priceAddress: USDC_ADDRESS,
1068
- fixedPriceUsd: 1
1069
- },
1070
- {
1071
- name: "ZORA",
1072
- symbol: "ZORA",
1073
- address: ZORA_ADDRESS,
1074
- decimals: 18,
1075
- priceAddress: ZORA_ADDRESS
1076
- }
1077
- ];
1078
- var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
1079
- try {
1080
- const res = await getTokenInfo({ address, chainId });
1081
- return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
1082
- } catch (err) {
1083
- console.warn(
1084
- `Warning: failed to fetch price for ${address}: ${formatError(err)}`
1085
- );
1086
- return null;
1087
- }
1088
- };
1089
- var fetchWalletBalances = async (walletAddress) => {
1090
- const publicClient = createPublicClient2({ chain: base2, transport: http() });
1091
- const nativeToken = TRACKED_TOKENS.find((t) => t.isNative);
1092
- const erc20Tokens = TRACKED_TOKENS.filter((t) => !t.isNative);
1093
- const [ethBalance, multicallResults] = await Promise.all([
1094
- publicClient.getBalance({ address: walletAddress }),
1095
- publicClient.multicall({
1096
- contracts: erc20Tokens.map((t) => ({
1097
- address: t.address,
1098
- abi: erc20Abi,
1099
- functionName: "balanceOf",
1100
- args: [walletAddress]
1101
- }))
1102
- })
1103
- ]);
1104
- const rawBalances = /* @__PURE__ */ new Map();
1105
- if (nativeToken) rawBalances.set(nativeToken, ethBalance);
1106
- erc20Tokens.forEach((token, i) => {
1107
- if (multicallResults[i].status === "success") {
1108
- rawBalances.set(token, multicallResults[i].result);
1109
- } else {
1110
- console.warn(`Warning: failed to fetch balance for ${token.symbol}`);
1111
- rawBalances.set(token, 0n);
1112
- }
1113
- });
1114
- const priceResults = await Promise.allSettled(
1115
- TRACKED_TOKENS.map(async (token) => {
1116
- const balance = rawBalances.get(token) ?? 0n;
1117
- let priceUsd = null;
1118
- if (token.fixedPriceUsd != null) {
1119
- priceUsd = token.fixedPriceUsd;
1120
- } else if (balance > 0n || token.isNative) {
1121
- priceUsd = await fetchTokenPriceUsd(token.priceAddress);
1122
- }
1123
- return { token, balance, priceUsd };
1288
+ useState as useState3,
1289
+ useEffect as useEffect3,
1290
+ useCallback as useCallback3,
1291
+ useRef as useRef2
1292
+ } from "react";
1293
+ import { Box as Box3, Text as Text3, useInput as useInput2, useApp as useApp2 } from "ink";
1294
+ import Spinner2 from "ink-spinner";
1295
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1296
+ var CACHE_KEY_FIRST = "__first__";
1297
+ var CACHE_TTL_MS = 6e4;
1298
+ function PaginatedTableView({
1299
+ fetchPage,
1300
+ columns,
1301
+ title,
1302
+ loadingText,
1303
+ emptyState: emptyState8,
1304
+ getAddress,
1305
+ limit = 10,
1306
+ initialCursor,
1307
+ autoRefresh = false,
1308
+ intervalSeconds = 30,
1309
+ formatSubtitle
1310
+ }) {
1311
+ const { exit } = useApp2();
1312
+ const [loading, setLoading] = useState3(true);
1313
+ const [error, setError] = useState3(null);
1314
+ const [items, setItems] = useState3([]);
1315
+ const [total, setTotal] = useState3(0);
1316
+ const [pageInfo, setPageInfo] = useState3(null);
1317
+ const [page, setPage] = useState3(1);
1318
+ const targetPage = useRef2(1);
1319
+ const [cursorHistory, setCursorHistory] = useState3(
1320
+ []
1321
+ );
1322
+ const [currentCursor, setCurrentCursor] = useState3(
1323
+ initialCursor
1324
+ );
1325
+ const cache = useRef2(/* @__PURE__ */ new Map());
1326
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
1327
+ const [selectedRow, setSelectedRow] = useState3(0);
1328
+ const [copyFeedback, setCopyFeedback] = useState3(null);
1329
+ useEffect3(() => {
1330
+ setSelectedRow((r) => Math.min(r, Math.max(0, items.length - 1)));
1331
+ }, [items.length]);
1332
+ const loadPage = useCallback3(
1333
+ async (cursor) => {
1334
+ const cacheKey = cursor ?? CACHE_KEY_FIRST;
1335
+ const cached = cache.current.get(cacheKey);
1336
+ const isFresh = cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS;
1337
+ if (isFresh) {
1338
+ setItems(cached.result.items);
1339
+ setTotal(cached.result.count ?? cached.result.items.length);
1340
+ setPageInfo(cached.result.pageInfo ?? null);
1341
+ setPage(targetPage.current);
1342
+ setError(null);
1343
+ setLoading(false);
1344
+ return;
1345
+ }
1346
+ setLoading(true);
1347
+ setError(null);
1348
+ try {
1349
+ const result = await fetchPage(cursor);
1350
+ cache.current.set(cacheKey, { result, fetchedAt: Date.now() });
1351
+ setItems(result.items);
1352
+ setTotal(result.count ?? result.items.length);
1353
+ setPageInfo(result.pageInfo ?? null);
1354
+ setPage(targetPage.current);
1355
+ } catch (err) {
1356
+ setError(err instanceof Error ? err.message : String(err));
1357
+ }
1358
+ setLoading(false);
1359
+ },
1360
+ [fetchPage]
1361
+ );
1362
+ useEffect3(() => {
1363
+ if (refreshCount === 0) return;
1364
+ const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
1365
+ cache.current.delete(cacheKey);
1366
+ }, [refreshCount, currentCursor]);
1367
+ useEffect3(() => {
1368
+ loadPage(currentCursor);
1369
+ }, [currentCursor, loadPage, refreshCount]);
1370
+ useInput2((input, key) => {
1371
+ if (input === "q" || key.escape) {
1372
+ exit();
1373
+ return;
1374
+ }
1375
+ if (loading) return;
1376
+ if (key.upArrow || input === "k") {
1377
+ setSelectedRow((r) => Math.max(0, r - 1));
1378
+ return;
1379
+ }
1380
+ if (key.downArrow || input === "j") {
1381
+ setSelectedRow((r) => Math.min(items.length - 1, r + 1));
1382
+ return;
1383
+ }
1384
+ if (input === "c" || key.return) {
1385
+ const item = items[selectedRow];
1386
+ if (item) {
1387
+ const address = getAddress(item);
1388
+ if (address) {
1389
+ const ok = copyToClipboard(address);
1390
+ setCopyFeedback(ok ? "Copied!" : "Copy failed");
1391
+ setTimeout(() => setCopyFeedback(null), 1500);
1392
+ }
1393
+ }
1394
+ return;
1395
+ }
1396
+ const canGoNext = pageInfo?.hasNextPage && pageInfo.endCursor;
1397
+ const canGoPrev = cursorHistory.length > 0;
1398
+ if ((input === "n" || key.rightArrow) && canGoNext) {
1399
+ setCursorHistory((prev) => [...prev, currentCursor]);
1400
+ setCurrentCursor(pageInfo?.endCursor);
1401
+ targetPage.current += 1;
1402
+ setSelectedRow(0);
1403
+ }
1404
+ if ((input === "p" || key.leftArrow) && canGoPrev) {
1405
+ const prev = cursorHistory[cursorHistory.length - 1];
1406
+ setCursorHistory((h) => h.slice(0, -1));
1407
+ setCurrentCursor(prev);
1408
+ targetPage.current -= 1;
1409
+ setSelectedRow(0);
1410
+ }
1411
+ if (input === "r") {
1412
+ const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
1413
+ cache.current.delete(cacheKey);
1414
+ triggerManualRefresh();
1415
+ setSelectedRow(0);
1416
+ }
1417
+ });
1418
+ if (error) {
1419
+ return /* @__PURE__ */ jsxs3(
1420
+ Box3,
1421
+ {
1422
+ flexDirection: "column",
1423
+ paddingLeft: 1,
1424
+ paddingTop: 1,
1425
+ paddingBottom: 1,
1426
+ children: [
1427
+ /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
1428
+ "Error: ",
1429
+ error
1430
+ ] }),
1431
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Press q to exit" }) })
1432
+ ]
1433
+ }
1434
+ );
1435
+ }
1436
+ if (loading) {
1437
+ return /* @__PURE__ */ jsx3(Box3, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
1438
+ /* @__PURE__ */ jsx3(Spinner2, { type: "dots" }),
1439
+ " ",
1440
+ loadingText
1441
+ ] }) });
1442
+ }
1443
+ if (items.length === 0) {
1444
+ return /* @__PURE__ */ jsx3(Fragment, { children: emptyState8 });
1445
+ }
1446
+ const rankedItems = items.map((item, i) => ({
1447
+ ...item,
1448
+ rank: (page - 1) * limit + i + 1
1449
+ }));
1450
+ const subtitle = formatSubtitle ? formatSubtitle({ page, itemCount: items.length, total }) : `Page ${page} \xB7 ${items.length} of ${total}`;
1451
+ const hints = [];
1452
+ hints.push("\u2191\u2193 select");
1453
+ hints.push("enter/c copy address");
1454
+ if (cursorHistory.length > 0) hints.push("\u2190 prev");
1455
+ if (pageInfo?.hasNextPage) hints.push("\u2192 next");
1456
+ hints.push(autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh");
1457
+ hints.push("q quit");
1458
+ const footer = hints.join(" \xB7 ") + (copyFeedback ? ` ${copyFeedback}` : "");
1459
+ return /* @__PURE__ */ jsx3(
1460
+ Table,
1461
+ {
1462
+ data: rankedItems,
1463
+ columns,
1464
+ title,
1465
+ subtitle,
1466
+ footer,
1467
+ selectedRow
1468
+ }
1469
+ );
1470
+ }
1471
+
1472
+ // src/components/BalanceCoinsView.tsx
1473
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1474
+ var emptyState = /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
1475
+ /* @__PURE__ */ jsx4(Text4, { children: "No coin balances found." }),
1476
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
1477
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Buy coins to see them here:" }),
1478
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1479
+ " zora buy ",
1480
+ "<address>",
1481
+ " --eth 0.001"
1482
+ ] })
1483
+ ] }),
1484
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
1485
+ ] });
1486
+ var BalanceCoinsView = ({
1487
+ fetchPage,
1488
+ sort,
1489
+ limit,
1490
+ initialCursor,
1491
+ autoRefresh,
1492
+ intervalSeconds,
1493
+ hasApiKey = false
1494
+ }) => {
1495
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1496
+ /* @__PURE__ */ jsx4(
1497
+ PaginatedTableView,
1498
+ {
1499
+ fetchPage,
1500
+ columns: balanceColumns,
1501
+ title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
1502
+ loadingText: "Loading\u2026",
1503
+ emptyState,
1504
+ getAddress: (item) => item.coin?.address,
1505
+ limit,
1506
+ initialCursor,
1507
+ autoRefresh,
1508
+ intervalSeconds
1509
+ }
1510
+ ),
1511
+ !hasApiKey && /* @__PURE__ */ jsx4(Box4, { paddingLeft: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: API_KEY_BANNER }) })
1512
+ ] });
1513
+ };
1514
+
1515
+ // src/lib/wallet-balances.ts
1516
+ import { getTokenInfo } from "@zoralabs/coins-sdk";
1517
+ import {
1518
+ createPublicClient as createPublicClient2,
1519
+ erc20Abi,
1520
+ formatUnits as formatUnits2,
1521
+ http
1522
+ } from "viem";
1523
+ import { base as base2 } from "viem/chains";
1524
+ var TRACKED_TOKENS = [
1525
+ {
1526
+ name: "Ether",
1527
+ symbol: "ETH",
1528
+ address: WETH_ADDRESS,
1529
+ decimals: 18,
1530
+ priceAddress: WETH_ADDRESS,
1531
+ isNative: true
1532
+ },
1533
+ {
1534
+ name: "USD Coin",
1535
+ symbol: "USDC",
1536
+ address: USDC_ADDRESS,
1537
+ decimals: USDC_DECIMALS,
1538
+ priceAddress: USDC_ADDRESS,
1539
+ fixedPriceUsd: 1
1540
+ },
1541
+ {
1542
+ name: "ZORA",
1543
+ symbol: "ZORA",
1544
+ address: ZORA_ADDRESS,
1545
+ decimals: 18,
1546
+ priceAddress: ZORA_ADDRESS
1547
+ }
1548
+ ];
1549
+ var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
1550
+ try {
1551
+ const res = await getTokenInfo({ address, chainId });
1552
+ return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
1553
+ } catch (err) {
1554
+ console.warn(
1555
+ `Warning: failed to fetch price for ${address}: ${formatError(err)}`
1556
+ );
1557
+ return null;
1558
+ }
1559
+ };
1560
+ var fetchWalletBalances = async (walletAddress) => {
1561
+ const publicClient = createPublicClient2({ chain: base2, transport: http() });
1562
+ const nativeToken = TRACKED_TOKENS.find((t) => t.isNative);
1563
+ const erc20Tokens = TRACKED_TOKENS.filter((t) => !t.isNative);
1564
+ const [ethBalance, multicallResults] = await Promise.all([
1565
+ publicClient.getBalance({ address: walletAddress }),
1566
+ publicClient.multicall({
1567
+ contracts: erc20Tokens.map((t) => ({
1568
+ address: t.address,
1569
+ abi: erc20Abi,
1570
+ functionName: "balanceOf",
1571
+ args: [walletAddress]
1572
+ }))
1573
+ })
1574
+ ]);
1575
+ const rawBalances = /* @__PURE__ */ new Map();
1576
+ if (nativeToken) rawBalances.set(nativeToken, ethBalance);
1577
+ erc20Tokens.forEach((token, i) => {
1578
+ if (multicallResults[i].status === "success") {
1579
+ rawBalances.set(token, multicallResults[i].result);
1580
+ } else {
1581
+ console.warn(`Warning: failed to fetch balance for ${token.symbol}`);
1582
+ rawBalances.set(token, 0n);
1583
+ }
1584
+ });
1585
+ const priceResults = await Promise.allSettled(
1586
+ TRACKED_TOKENS.map(async (token) => {
1587
+ const balance = rawBalances.get(token) ?? 0n;
1588
+ let priceUsd = null;
1589
+ if (token.fixedPriceUsd != null) {
1590
+ priceUsd = token.fixedPriceUsd;
1591
+ } else if (balance > 0n || token.isNative) {
1592
+ priceUsd = await fetchTokenPriceUsd(token.priceAddress);
1593
+ }
1594
+ return { token, balance, priceUsd };
1124
1595
  })
1125
1596
  );
1126
1597
  const resolved = priceResults.map((result, i) => {
@@ -1157,20 +1628,14 @@ var fetchWalletBalances = async (walletAddress) => {
1157
1628
  };
1158
1629
 
1159
1630
  // src/commands/balance.tsx
1160
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1631
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1161
1632
  var SORT_MAP = {
1162
1633
  "usd-value": "USD_VALUE",
1163
1634
  balance: "BALANCE",
1164
1635
  "market-cap": "MARKET_CAP",
1165
1636
  "price-change": "PRICE_CHANGE"
1166
1637
  };
1167
- var SORT_OPTIONS = Object.keys(SORT_LABELS).join(", ");
1168
- var extractErrorMessage = (error) => {
1169
- if (typeof error === "object" && error !== null && "error" in error) {
1170
- return String(error.error);
1171
- }
1172
- return JSON.stringify(error);
1173
- };
1638
+ var SORT_OPTIONS = Object.keys(SORT_LABELS2).join(", ");
1174
1639
  var formatBalanceJson = (balance, rank) => {
1175
1640
  const priceUsd = balance.coin?.tokenPrice?.priceInUsdc;
1176
1641
  const marketCap = balance.coin?.marketCap ? Number(balance.coin.marketCap) : null;
@@ -1178,7 +1643,11 @@ var formatBalanceJson = (balance, rank) => {
1178
1643
  const volume24h = balance.coin?.volume24h ? Number(balance.coin.volume24h) : null;
1179
1644
  const totalVolume = balance.coin?.totalVolume ? Number(balance.coin.totalVolume) : null;
1180
1645
  const priceUsdValue = priceUsd ? Number(priceUsd) : null;
1181
- const usdValue = priceUsdValue !== null ? Number((parseRawBalance(balance.balance) * priceUsdValue).toFixed(6)) : null;
1646
+ const usdValue = computeBalanceUsdValue(
1647
+ balance.balance,
1648
+ balance.valuation?.marketValueUsd,
1649
+ priceUsd
1650
+ );
1182
1651
  const marketCapChange24h = computeMarketCapChange24h(
1183
1652
  marketCap,
1184
1653
  marketCapDelta24h
@@ -1187,6 +1656,7 @@ var formatBalanceJson = (balance, rank) => {
1187
1656
  rank,
1188
1657
  name: balance.coin?.name ?? null,
1189
1658
  symbol: balance.coin?.symbol ?? null,
1659
+ type: formatCoinType(balance.coin?.coinType) || null,
1190
1660
  coinType: balance.coin?.coinType ?? null,
1191
1661
  chainId: balance.coin?.chainId ?? null,
1192
1662
  address: balance.coin?.address ?? null,
@@ -1208,14 +1678,14 @@ function resolveContext(json) {
1208
1678
  if (apiKey) {
1209
1679
  setApiKey(apiKey);
1210
1680
  }
1211
- return account;
1681
+ return { account, hasApiKey: !!apiKey };
1212
1682
  }
1213
1683
  function renderWallet(json, walletResult) {
1214
1684
  outputData(json, {
1215
1685
  json: { wallet: walletResult.walletBalancesJson },
1216
1686
  render: () => {
1217
1687
  renderOnce(
1218
- /* @__PURE__ */ jsx3(
1688
+ /* @__PURE__ */ jsx5(
1219
1689
  Table,
1220
1690
  {
1221
1691
  columns: walletColumns,
@@ -1227,7 +1697,7 @@ function renderWallet(json, walletResult) {
1227
1697
  }
1228
1698
  });
1229
1699
  }
1230
- function renderCoins(json, balances, total, sort, limit, pageInfo) {
1700
+ function renderCoins(json, balances, total, sort, limit, pageInfo, hasApiKey) {
1231
1701
  const rankedBalances = balances.map((balance, index) => ({
1232
1702
  ...balance,
1233
1703
  rank: index + 1
@@ -1247,16 +1717,19 @@ function renderCoins(json, balances, total, sort, limit, pageInfo) {
1247
1717
  } else {
1248
1718
  const footer = pageInfo?.hasNextPage && pageInfo.endCursor ? `Next page: zora balance coins --sort ${sort} --limit ${limit} --after ${pageInfo.endCursor}` : void 0;
1249
1719
  renderOnce(
1250
- /* @__PURE__ */ jsx3(
1251
- Table,
1252
- {
1253
- columns: balanceColumns,
1254
- data: rankedBalances,
1255
- title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1256
- subtitle: `${balances.length} of ${total}`,
1257
- footer
1258
- }
1259
- )
1720
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1721
+ /* @__PURE__ */ jsx5(
1722
+ Table,
1723
+ {
1724
+ columns: balanceColumns,
1725
+ data: rankedBalances,
1726
+ title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
1727
+ subtitle: `${balances.length} of ${total}`,
1728
+ footer
1729
+ }
1730
+ ),
1731
+ !hasApiKey && /* @__PURE__ */ jsx5(Box5, { paddingLeft: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: API_KEY_BANNER }) })
1732
+ ] })
1260
1733
  );
1261
1734
  }
1262
1735
  }
@@ -1312,7 +1785,7 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1312
1785
  ).action(async function() {
1313
1786
  const output = getOutputMode(this, "live");
1314
1787
  const json = output === "json";
1315
- const account = resolveContext(json);
1788
+ const { account, hasApiKey } = resolveContext(json);
1316
1789
  const { live, intervalSeconds } = getLiveConfig(this, output);
1317
1790
  const sort = "usd-value";
1318
1791
  const limit = 10;
@@ -1362,14 +1835,15 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1362
1835
  });
1363
1836
  } else if (live) {
1364
1837
  await renderLive(
1365
- /* @__PURE__ */ jsx3(
1838
+ /* @__PURE__ */ jsx5(
1366
1839
  BalanceView,
1367
1840
  {
1368
1841
  fetchData: fetchBalanceData,
1369
1842
  sort,
1370
1843
  mode: "full",
1371
1844
  autoRefresh: live,
1372
- intervalSeconds
1845
+ intervalSeconds,
1846
+ hasApiKey
1373
1847
  }
1374
1848
  )
1375
1849
  );
@@ -1385,8 +1859,8 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1385
1859
  (err) => outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`)
1386
1860
  );
1387
1861
  renderOnce(
1388
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1389
- /* @__PURE__ */ jsx3(
1862
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1863
+ /* @__PURE__ */ jsx5(
1390
1864
  Table,
1391
1865
  {
1392
1866
  columns: walletColumns,
@@ -1394,18 +1868,18 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1394
1868
  title: "Wallet"
1395
1869
  }
1396
1870
  ),
1397
- data.rankedBalances.length === 0 ? /* @__PURE__ */ jsxs3(
1398
- Box3,
1871
+ data.rankedBalances.length === 0 ? /* @__PURE__ */ jsxs5(
1872
+ Box5,
1399
1873
  {
1400
1874
  flexDirection: "column",
1401
1875
  paddingLeft: 1,
1402
1876
  paddingTop: 1,
1403
1877
  paddingBottom: 1,
1404
1878
  children: [
1405
- /* @__PURE__ */ jsx3(Text3, { children: "No coin balances found." }),
1406
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1407
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Buy coins to see them here:" }),
1408
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1879
+ /* @__PURE__ */ jsx5(Text5, { children: "No coin balances found." }),
1880
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
1881
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Buy coins to see them here:" }),
1882
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1409
1883
  " zora buy ",
1410
1884
  "<address>",
1411
1885
  " --eth 0.001"
@@ -1413,15 +1887,18 @@ var balanceCommand = new Command2("balance").description("Show balances in your
1413
1887
  ] })
1414
1888
  ]
1415
1889
  }
1416
- ) : /* @__PURE__ */ jsx3(
1417
- Table,
1418
- {
1419
- columns: balanceColumns,
1420
- data: data.rankedBalances,
1421
- title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
1422
- subtitle: `${data.rankedBalances.length} of ${data.total}`
1423
- }
1424
- )
1890
+ ) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1891
+ /* @__PURE__ */ jsx5(
1892
+ Table,
1893
+ {
1894
+ columns: balanceColumns,
1895
+ data: data.rankedBalances,
1896
+ title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
1897
+ subtitle: `${data.rankedBalances.length} of ${data.total}`
1898
+ }
1899
+ ),
1900
+ !hasApiKey && /* @__PURE__ */ jsx5(Box5, { paddingLeft: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: API_KEY_BANNER }) })
1901
+ ] })
1425
1902
  ] })
1426
1903
  );
1427
1904
  track("cli_balances", {
@@ -1441,7 +1918,7 @@ balanceCommand.command("spendable").description("Show wallet token balances (ETH
1441
1918
  ).action(async function() {
1442
1919
  const output = getOutputMode(this, "live");
1443
1920
  const json = output === "json";
1444
- const account = resolveContext(json);
1921
+ const { account } = resolveContext(json);
1445
1922
  const { live, intervalSeconds } = getLiveConfig(this, output);
1446
1923
  const fetchSpendableData = async () => {
1447
1924
  const walletResult = await fetchWalletBalances(account.address);
@@ -1463,7 +1940,7 @@ balanceCommand.command("spendable").description("Show wallet token balances (ETH
1463
1940
  });
1464
1941
  } else if (live) {
1465
1942
  await renderLive(
1466
- /* @__PURE__ */ jsx3(
1943
+ /* @__PURE__ */ jsx5(
1467
1944
  BalanceView,
1468
1945
  {
1469
1946
  fetchData: fetchSpendableData,
@@ -1490,7 +1967,7 @@ balanceCommand.command("coins").description("Show coin positions").option("--sor
1490
1967
  const json = output === "json";
1491
1968
  const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
1492
1969
  const after = opts.after;
1493
- const account = resolveContext(json);
1970
+ const { account, hasApiKey } = resolveContext(json);
1494
1971
  const { live, intervalSeconds } = getLiveConfig(this, output);
1495
1972
  const fetchCoinsPage = async (cursor) => {
1496
1973
  const { balances, total, pageInfo } = await fetchCoins(
@@ -1500,39 +1977,23 @@ balanceCommand.command("coins").description("Show coin positions").option("--sor
1500
1977
  limit,
1501
1978
  cursor
1502
1979
  );
1503
- const rankedBalances = balances.map((balance, index) => ({
1504
- ...balance,
1505
- rank: index + 1
1506
- }));
1507
- return {
1508
- walletBalances: [],
1509
- walletBalancesJson: [],
1510
- rankedBalances,
1511
- total,
1512
- pageInfo
1513
- };
1980
+ return { items: balances, count: total, pageInfo };
1514
1981
  };
1515
1982
  if (json) {
1516
- const data = await fetchCoinsPage(after);
1517
- renderCoins(
1518
- json,
1519
- data.rankedBalances,
1520
- data.total,
1521
- sort,
1522
- limit,
1523
- data.pageInfo
1524
- );
1983
+ const { items, count, pageInfo } = await fetchCoinsPage(after);
1984
+ renderCoins(json, items, count ?? items.length, sort, limit, pageInfo);
1525
1985
  } else if (live) {
1526
1986
  await renderLive(
1527
- /* @__PURE__ */ jsx3(
1528
- BalanceView,
1987
+ /* @__PURE__ */ jsx5(
1988
+ BalanceCoinsView,
1529
1989
  {
1530
- fetchData: fetchCoinsPage,
1990
+ fetchPage: fetchCoinsPage,
1531
1991
  sort,
1532
- mode: "coins",
1992
+ limit,
1533
1993
  initialCursor: after,
1534
1994
  autoRefresh: live,
1535
- intervalSeconds
1995
+ intervalSeconds,
1996
+ hasApiKey
1536
1997
  }
1537
1998
  )
1538
1999
  );
@@ -1544,7 +2005,7 @@ balanceCommand.command("coins").description("Show coin positions").option("--sor
1544
2005
  limit,
1545
2006
  after
1546
2007
  );
1547
- renderCoins(json, balances, total, sort, limit, pageInfo);
2008
+ renderCoins(json, balances, total, sort, limit, pageInfo, hasApiKey);
1548
2009
  }
1549
2010
  });
1550
2011
 
@@ -1761,6 +2222,19 @@ function coinArgsToRef(parsed) {
1761
2222
  return { kind: "ambiguous", name: parsed.name };
1762
2223
  }
1763
2224
  }
2225
+ async function resolveAmbiguousByNameAndBalance(name, getBalance) {
2226
+ const result = await resolveAmbiguousName(name);
2227
+ if (result.kind !== "ambiguous") return result;
2228
+ const [creatorBal, trendBal] = await Promise.all([
2229
+ getBalance(result.creator.address),
2230
+ getBalance(result.trend.address)
2231
+ ]);
2232
+ if (creatorBal > 0n && trendBal === 0n)
2233
+ return { kind: "found", coin: result.creator };
2234
+ if (trendBal > 0n && creatorBal === 0n)
2235
+ return { kind: "found", coin: result.trend };
2236
+ return result;
2237
+ }
1764
2238
  async function resolveAmbiguousName(name) {
1765
2239
  const [creatorResult, trendResult] = await Promise.all([
1766
2240
  resolveByCreatorName(name),
@@ -1811,6 +2285,7 @@ function coinFromToken(token) {
1811
2285
  marketCap: token.marketCap ?? "0",
1812
2286
  marketCapDelta24h: token.marketCapDelta24h ?? "0",
1813
2287
  volume24h: token.volume24h ?? "0",
2288
+ totalSupply: token.totalSupply ?? "0",
1814
2289
  uniqueHolders: token.uniqueHolders ?? 0,
1815
2290
  createdAt: token.createdAt,
1816
2291
  creatorAddress: token.creatorAddress,
@@ -2138,7 +2613,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
2138
2613
  if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
2139
2614
  if (json) {
2140
2615
  outputJson({ error: errorBody ?? msg });
2141
- process.exit(1);
2616
+ safeExit(ERROR);
2142
2617
  }
2143
2618
  outputErrorAndExit(
2144
2619
  json,
@@ -2186,7 +2661,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
2186
2661
  default: false
2187
2662
  });
2188
2663
  if (!ok) {
2189
- process.exit(0);
2664
+ safeExit(SUCCESS);
2190
2665
  }
2191
2666
  }
2192
2667
  let receipt;
@@ -2290,66 +2765,16 @@ import {
2290
2765
  getTrendingTrends
2291
2766
  } from "@zoralabs/coins-sdk";
2292
2767
 
2293
- // src/lib/types.ts
2294
- var SORT_LABELS2 = {
2295
- mcap: "Top by Market Cap",
2296
- volume: "Top by 24h Volume",
2297
- new: "New",
2298
- trending: "Trending",
2299
- featured: "Featured"
2300
- };
2301
- var TYPE_LABELS = {
2302
- all: "all",
2303
- trend: "trends",
2304
- "creator-coin": "creator coins",
2305
- post: "posts"
2306
- };
2307
- var COIN_TYPE_DISPLAY = {
2308
- CONTENT: "post",
2309
- CREATOR: "creator-coin",
2310
- TREND: "trend"
2311
- };
2312
-
2313
- // src/components/ExploreView.tsx
2314
- import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef2 } from "react";
2315
- import { Box as Box4, Text as Text4, useInput as useInput2, useApp as useApp2 } from "ink";
2316
- import Spinner2 from "ink-spinner";
2317
-
2318
- // src/lib/clipboard.ts
2319
- import { execFileSync } from "child_process";
2320
- import { platform as platform2 } from "os";
2321
- var copyToClipboard = (text) => {
2322
- const os = platform2();
2323
- try {
2324
- if (os === "darwin") {
2325
- execFileSync("pbcopy", {
2326
- input: text,
2327
- stdio: ["pipe", "ignore", "ignore"]
2328
- });
2329
- } else if (os === "linux") {
2330
- execFileSync("xclip", ["-selection", "clipboard"], {
2331
- input: text,
2332
- stdio: ["pipe", "ignore", "ignore"]
2333
- });
2334
- } else if (os === "win32") {
2335
- execFileSync("clip", {
2336
- input: text,
2337
- stdio: ["pipe", "ignore", "ignore"]
2338
- });
2339
- } else {
2340
- return false;
2341
- }
2342
- return true;
2343
- } catch {
2344
- return false;
2345
- }
2346
- };
2347
-
2348
2768
  // src/components/ExploreView.tsx
2349
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2769
+ import { Box as Box6, Text as Text6 } from "ink";
2770
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2350
2771
  var COLUMNS = [
2351
2772
  { header: "#", width: 4, accessor: (c) => String(c.rank) },
2352
- { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2773
+ {
2774
+ header: "Name",
2775
+ width: 20,
2776
+ accessor: (c) => formatCoinName(c)
2777
+ },
2353
2778
  {
2354
2779
  header: "Address",
2355
2780
  width: 14,
@@ -2358,7 +2783,7 @@ var COLUMNS = [
2358
2783
  {
2359
2784
  header: "Type",
2360
2785
  width: 14,
2361
- accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
2786
+ accessor: (c) => formatCoinType(c.coinType)
2362
2787
  },
2363
2788
  {
2364
2789
  header: "Market Cap",
@@ -2377,191 +2802,49 @@ var COLUMNS = [
2377
2802
  color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
2378
2803
  }
2379
2804
  ];
2380
- var CACHE_TTL_MS = 6e4;
2381
- var CACHE_KEY_FIRST = "__first__";
2805
+ var emptyState2 = /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
2806
+ /* @__PURE__ */ jsx6(Text6, { children: "No coins found." }),
2807
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
2808
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Try a different sort or type:" }),
2809
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " zora explore --sort volume --type all" }),
2810
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " zora explore --sort new --type all" })
2811
+ ] }),
2812
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Press q to exit" }) })
2813
+ ] });
2382
2814
  var ExploreView = ({
2383
2815
  fetchPage,
2384
2816
  sort,
2385
2817
  type,
2386
2818
  limit,
2387
2819
  initialCursor,
2388
- cacheTtlMs = CACHE_TTL_MS,
2389
2820
  autoRefresh = false,
2390
2821
  intervalSeconds = 30
2391
2822
  }) => {
2392
- const { exit } = useApp2();
2393
- const [loading, setLoading] = useState3(true);
2394
- const [error, setError] = useState3(null);
2395
- const [coins, setCoins] = useState3([]);
2396
- const [pageInfo, setPageInfo] = useState3(null);
2397
- const [page, setPage] = useState3(1);
2398
- const [cursorHistory, setCursorHistory] = useState3(
2399
- []
2400
- );
2401
- const [currentCursor, setCurrentCursor] = useState3(
2402
- initialCursor
2403
- );
2404
- const cache = useRef2(/* @__PURE__ */ new Map());
2405
- const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
2406
- const [manualRefreshCount, setManualRefreshCount] = useState3(0);
2407
- const [selectedRow, setSelectedRow] = useState3(0);
2408
- const [copyFeedback, setCopyFeedback] = useState3(null);
2409
- useEffect3(() => {
2410
- setSelectedRow((r) => Math.min(r, Math.max(0, coins.length - 1)));
2411
- }, [coins.length]);
2412
- const loadPage = useCallback3(
2413
- async (cursor) => {
2414
- const cacheKey = cursor ?? CACHE_KEY_FIRST;
2415
- const cached = cache.current.get(cacheKey);
2416
- const isFresh = cached && Date.now() - cached.fetchedAt < cacheTtlMs;
2417
- if (isFresh) {
2418
- setCoins(cached.result.coins);
2419
- setPageInfo(cached.result.pageInfo ?? null);
2420
- setError(null);
2421
- setLoading(false);
2422
- return;
2423
- }
2424
- setLoading(true);
2425
- setError(null);
2426
- try {
2427
- const result = await fetchPage(cursor);
2428
- cache.current.set(cacheKey, { result, fetchedAt: Date.now() });
2429
- setCoins(result.coins);
2430
- setPageInfo(result.pageInfo ?? null);
2431
- } catch (err) {
2432
- setError(err instanceof Error ? err.message : String(err));
2433
- }
2434
- setLoading(false);
2435
- },
2436
- [fetchPage, cacheTtlMs]
2437
- );
2438
- useEffect3(() => {
2439
- if (refreshCount === 0) return;
2440
- const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
2441
- cache.current.delete(cacheKey);
2442
- }, [refreshCount, currentCursor]);
2443
- useEffect3(() => {
2444
- loadPage(currentCursor);
2445
- }, [currentCursor, loadPage, refreshCount, manualRefreshCount]);
2446
- useInput2((input, key) => {
2447
- if (input === "q" || key.escape) {
2448
- exit();
2449
- return;
2450
- }
2451
- if (loading) return;
2452
- if (key.upArrow || input === "k") {
2453
- setSelectedRow((r) => Math.max(0, r - 1));
2454
- return;
2455
- }
2456
- if (key.downArrow || input === "j") {
2457
- setSelectedRow((r) => Math.min(coins.length - 1, r + 1));
2458
- return;
2459
- }
2460
- if (input === "c") {
2461
- const coin = coins[selectedRow];
2462
- if (coin?.address) {
2463
- const ok = copyToClipboard(coin.address);
2464
- setCopyFeedback(ok ? "Copied!" : "Copy failed");
2465
- setTimeout(() => setCopyFeedback(null), 1500);
2466
- }
2467
- return;
2468
- }
2469
- const canGoNext = pageInfo?.hasNextPage && pageInfo.endCursor;
2470
- const canGoPrev = cursorHistory.length > 0;
2471
- if ((input === "n" || key.rightArrow) && canGoNext) {
2472
- setCursorHistory((prev) => [...prev, currentCursor]);
2473
- setCurrentCursor(pageInfo.endCursor);
2474
- setPage((p) => p + 1);
2475
- setSelectedRow(0);
2476
- }
2477
- if ((input === "p" || key.leftArrow) && canGoPrev) {
2478
- const prev = cursorHistory[cursorHistory.length - 1];
2479
- setCursorHistory((h) => h.slice(0, -1));
2480
- setCurrentCursor(prev);
2481
- setPage((p) => p - 1);
2482
- setSelectedRow(0);
2483
- }
2484
- if (input === "r") {
2485
- const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
2486
- cache.current.delete(cacheKey);
2487
- triggerManualRefresh();
2488
- setManualRefreshCount((c) => c + 1);
2489
- setSelectedRow(0);
2490
- }
2491
- });
2492
- if (error) {
2493
- return /* @__PURE__ */ jsxs4(
2494
- Box4,
2495
- {
2496
- flexDirection: "column",
2497
- paddingLeft: 1,
2498
- paddingTop: 1,
2499
- paddingBottom: 1,
2500
- children: [
2501
- /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
2502
- "Error: ",
2503
- error
2504
- ] }),
2505
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
2506
- ]
2507
- }
2508
- );
2509
- }
2510
- if (loading) {
2511
- return /* @__PURE__ */ jsx4(Box4, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { children: [
2512
- /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
2513
- " Loading\u2026"
2514
- ] }) });
2515
- }
2516
- if (coins.length === 0) {
2517
- return /* @__PURE__ */ jsxs4(
2518
- Box4,
2519
- {
2520
- flexDirection: "column",
2521
- paddingLeft: 1,
2522
- paddingTop: 1,
2523
- paddingBottom: 1,
2524
- children: [
2525
- /* @__PURE__ */ jsx4(Text4, { children: "No coins found." }),
2526
- /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
2527
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Try a different sort or type:" }),
2528
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " zora explore --sort volume --type all" }),
2529
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " zora explore --sort new --type all" })
2530
- ] }),
2531
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
2532
- ]
2533
- }
2534
- );
2535
- }
2536
- const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
2537
- const subtitle = `Page ${page} \xB7 ${coins.length} result${coins.length !== 1 ? "s" : ""}`;
2538
- const rankedCoins = coins.map((c, i) => ({
2539
- ...c,
2540
- rank: (page - 1) * limit + i + 1
2541
- }));
2542
- const hints = [];
2543
- hints.push("\u2191\u2193 select");
2544
- hints.push("c copy address");
2545
- if (cursorHistory.length > 0) hints.push("\u2190 prev");
2546
- if (pageInfo?.hasNextPage) hints.push("\u2192 next");
2547
- hints.push(autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh");
2548
- hints.push("q quit");
2549
- const footer = hints.join(" \xB7 ") + (copyFeedback ? ` ${copyFeedback}` : "");
2550
- return /* @__PURE__ */ jsx4(
2551
- Table,
2823
+ const title = type !== "all" ? `${SORT_LABELS[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS[sort];
2824
+ const formatSubtitle = ({
2825
+ page,
2826
+ itemCount
2827
+ }) => `Page ${page} \xB7 ${itemCount} result${itemCount !== 1 ? "s" : ""}`;
2828
+ return /* @__PURE__ */ jsx6(
2829
+ PaginatedTableView,
2552
2830
  {
2553
- data: rankedCoins,
2831
+ fetchPage,
2554
2832
  columns: COLUMNS,
2555
2833
  title,
2556
- subtitle,
2557
- footer,
2558
- selectedRow
2834
+ loadingText: "Loading\u2026",
2835
+ emptyState: emptyState2,
2836
+ getAddress: (coin) => coin.address,
2837
+ limit,
2838
+ initialCursor,
2839
+ autoRefresh,
2840
+ intervalSeconds,
2841
+ formatSubtitle
2559
2842
  }
2560
2843
  );
2561
2844
  };
2562
2845
 
2563
2846
  // src/commands/explore.tsx
2564
- import { jsx as jsx5 } from "react/jsx-runtime";
2847
+ import { jsx as jsx7 } from "react/jsx-runtime";
2565
2848
  var formatExploreCoinJson = (node) => {
2566
2849
  const marketCap = node.marketCap ? Number(node.marketCap) : null;
2567
2850
  const marketCapDelta24h = node.marketCapDelta24h ? Number(node.marketCapDelta24h) : null;
@@ -2570,7 +2853,7 @@ var formatExploreCoinJson = (node) => {
2570
2853
  marketCapDelta24h
2571
2854
  );
2572
2855
  const priceUsd = node.tokenPrice?.priceInUsdc ? Number(node.tokenPrice.priceInUsdc) : null;
2573
- const coinType = node.coinType ? COIN_TYPE_DISPLAY[node.coinType] ?? node.coinType : null;
2856
+ const coinType = formatCoinType(node.coinType) || null;
2574
2857
  const socials = node.creatorProfile?.socialAccounts;
2575
2858
  const socialAccounts = socials ? {
2576
2859
  instagram: socials.instagram ?? null,
@@ -2635,12 +2918,12 @@ var QUERY_MAP = {
2635
2918
  };
2636
2919
  var STATIC_COLUMNS = [
2637
2920
  { header: "#", width: 4, accessor: (c) => String(c.rank) },
2638
- { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
2921
+ { header: "Name", width: 20, accessor: (c) => formatCoinName(c) },
2639
2922
  { header: "Address", width: 48, accessor: (c) => c.address ?? "" },
2640
2923
  {
2641
2924
  header: "Type",
2642
2925
  width: 14,
2643
- accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
2926
+ accessor: (c) => formatCoinType(c.coinType)
2644
2927
  },
2645
2928
  {
2646
2929
  header: "Market Cap",
@@ -2659,7 +2942,7 @@ var STATIC_COLUMNS = [
2659
2942
  color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
2660
2943
  }
2661
2944
  ];
2662
- var SORT_OPTIONS2 = Object.keys(SORT_LABELS2).join(", ");
2945
+ var SORT_OPTIONS2 = Object.keys(SORT_LABELS).join(", ");
2663
2946
  var exploreCommand = new Command4("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "mcap").option(
2664
2947
  "--type <type>",
2665
2948
  "Filter by type: all, trend, creator-coin, post (availability varies by sort)",
@@ -2710,14 +2993,14 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2710
2993
  outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2711
2994
  }
2712
2995
  if (response.error) {
2713
- const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
2714
- outputErrorAndExit(json, `API error: ${msg}`);
2996
+ outputErrorAndExit(
2997
+ json,
2998
+ `API error: ${extractErrorMessage(response.error)}`
2999
+ );
2715
3000
  }
2716
3001
  const edges = response.data?.exploreList?.edges ?? [];
2717
3002
  const rawNodes = edges.map((e) => e.node);
2718
- const coins = rawNodes.map(
2719
- (node, i) => formatExploreCoinJson(node)
2720
- );
3003
+ const coins = rawNodes.map((node, i) => formatExploreCoinJson(node));
2721
3004
  const pageInfo = response.data?.exploreList?.pageInfo;
2722
3005
  outputJson({ coins, pageInfo: pageInfo ?? null });
2723
3006
  track("cli_explore", {
@@ -2734,17 +3017,16 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2734
3017
  const fetchPage = async (cursor) => {
2735
3018
  const response = await queryFn({ count: limit, after: cursor });
2736
3019
  if (response.error) {
2737
- const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
2738
- throw new Error(msg);
3020
+ throw new Error(extractErrorMessage(response.error));
2739
3021
  }
2740
3022
  const edges = response.data?.exploreList?.edges ?? [];
2741
- const coins = edges.map((e) => e.node);
3023
+ const items = edges.map((e) => e.node);
2742
3024
  const pageInfo = response.data?.exploreList?.pageInfo;
2743
- return { coins, pageInfo };
3025
+ return { items, pageInfo };
2744
3026
  };
2745
3027
  if (live) {
2746
3028
  await renderLive(
2747
- /* @__PURE__ */ jsx5(
3029
+ /* @__PURE__ */ jsx7(
2748
3030
  ExploreView,
2749
3031
  {
2750
3032
  fetchPage,
@@ -2767,19 +3049,19 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2767
3049
  output_format: "live"
2768
3050
  });
2769
3051
  } else {
2770
- const { coins } = await fetchPage(after).catch(
3052
+ const { items } = await fetchPage(after).catch(
2771
3053
  (err) => outputErrorAndExit(
2772
3054
  false,
2773
3055
  `Request failed: ${err instanceof Error ? err.message : String(err)}`
2774
3056
  )
2775
3057
  );
2776
- const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
2777
- const rankedCoins = coins.map((c, i) => ({
3058
+ const title = type !== "all" ? `${SORT_LABELS[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS[sort];
3059
+ const rankedCoins = items.map((c, i) => ({
2778
3060
  ...c,
2779
3061
  rank: i + 1
2780
3062
  }));
2781
3063
  renderOnce(
2782
- /* @__PURE__ */ jsx5(Table, { columns: STATIC_COLUMNS, data: rankedCoins, title })
3064
+ /* @__PURE__ */ jsx7(Table, { columns: STATIC_COLUMNS, data: rankedCoins, title })
2783
3065
  );
2784
3066
  track("cli_explore", {
2785
3067
  sort,
@@ -2787,7 +3069,7 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2787
3069
  limit,
2788
3070
  live: false,
2789
3071
  paginated: after !== void 0,
2790
- result_count: coins.length,
3072
+ result_count: items.length,
2791
3073
  output_format: "static"
2792
3074
  });
2793
3075
  }
@@ -2796,27 +3078,28 @@ var exploreCommand = new Command4("explore").description("Browse top, new, and h
2796
3078
 
2797
3079
  // src/commands/get.tsx
2798
3080
  import { Command as Command5 } from "commander";
2799
- import { setApiKey as setApiKey4 } from "@zoralabs/coins-sdk";
3081
+ import { Box as Box12, Text as Text12 } from "ink";
3082
+ import { setApiKey as setApiKey4, getCoinHolders, getCoinSwaps } from "@zoralabs/coins-sdk";
2800
3083
 
2801
3084
  // src/components/CoinDetail.tsx
2802
- import { Box as Box5, Text as Text5 } from "ink";
2803
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3085
+ import { Box as Box7, Text as Text7 } from "ink";
3086
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2804
3087
  var LABEL_WIDTH = 18;
2805
3088
  function Row({
2806
3089
  label,
2807
3090
  children
2808
3091
  }) {
2809
- return /* @__PURE__ */ jsxs5(Box5, { children: [
2810
- /* @__PURE__ */ jsx6(Box5, { width: LABEL_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: label }) }),
2811
- /* @__PURE__ */ jsx6(Text5, { children })
3092
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
3093
+ /* @__PURE__ */ jsx8(Box7, { width: LABEL_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: label }) }),
3094
+ /* @__PURE__ */ jsx8(Text7, { children })
2812
3095
  ] });
2813
3096
  }
2814
3097
  function CoinDetail({ coin }) {
2815
3098
  const change = formatMcapChange(coin.marketCap, coin.marketCapDelta24h);
2816
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingLeft: 1, children: [
2817
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
2818
- /* @__PURE__ */ jsx6(Text5, { bold: true, children: coin.name }),
2819
- /* @__PURE__ */ jsxs5(Text5, { children: [
3099
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 1, children: [
3100
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3101
+ /* @__PURE__ */ jsx8(Text7, { bold: true, children: coin.name }),
3102
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2820
3103
  coin.coinType,
2821
3104
  " ",
2822
3105
  "\xB7",
@@ -2824,175 +3107,423 @@ function CoinDetail({ coin }) {
2824
3107
  coin.address
2825
3108
  ] })
2826
3109
  ] }),
2827
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
2828
- /* @__PURE__ */ jsx6(Row, { label: "Market Cap", children: formatCompactUsd(coin.marketCap) }),
2829
- /* @__PURE__ */ jsx6(Row, { label: "24h Volume", children: formatCompactUsd(coin.volume24h) }),
2830
- /* @__PURE__ */ jsx6(Row, { label: "24h Change", children: /* @__PURE__ */ jsx6(Text5, { color: change.color, children: change.text }) }),
2831
- /* @__PURE__ */ jsx6(Row, { label: "Holders", children: formatHolders(coin.uniqueHolders) }),
2832
- coin.coinType === "post" && (coin.creatorHandle ?? coin.creatorAddress) && /* @__PURE__ */ jsx6(Row, { label: "Creator", children: coin.creatorHandle ?? coin.creatorAddress }),
2833
- /* @__PURE__ */ jsx6(Row, { label: "Created", children: formatCreatedAt(coin.createdAt) })
3110
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3111
+ /* @__PURE__ */ jsx8(Row, { label: "Market Cap", children: formatCompactUsd(coin.marketCap) }),
3112
+ /* @__PURE__ */ jsx8(Row, { label: "24h Volume", children: formatCompactUsd(coin.volume24h) }),
3113
+ /* @__PURE__ */ jsx8(Row, { label: "24h Change", children: /* @__PURE__ */ jsx8(Text7, { color: change.color, children: change.text }) }),
3114
+ /* @__PURE__ */ jsx8(Row, { label: "Holders", children: formatHolders(coin.uniqueHolders) }),
3115
+ coin.coinType === "post" && (coin.creatorHandle ?? coin.creatorAddress) && /* @__PURE__ */ jsx8(Row, { label: "Creator", children: coin.creatorHandle ?? coin.creatorAddress }),
3116
+ /* @__PURE__ */ jsx8(Row, { label: "Created", children: formatCreatedAt(coin.createdAt) })
2834
3117
  ] }),
2835
- /* @__PURE__ */ jsx6(Box5, { marginBottom: 1 })
3118
+ /* @__PURE__ */ jsx8(Box7, { marginBottom: 1 })
2836
3119
  ] });
2837
3120
  }
2838
3121
 
2839
- // src/commands/get.tsx
2840
- import { jsx as jsx7 } from "react/jsx-runtime";
2841
- function formatCoinJson(coin) {
2842
- return {
2843
- name: coin.name,
2844
- address: coin.address,
2845
- coinType: coin.coinType,
2846
- marketCap: coin.marketCap,
2847
- marketCapDelta24h: coin.marketCapDelta24h,
2848
- volume24h: coin.volume24h,
2849
- uniqueHolders: coin.uniqueHolders,
2850
- createdAt: coin.createdAt ?? null,
2851
- creatorAddress: coin.creatorAddress ?? null,
2852
- creatorHandle: coin.creatorHandle ?? null
2853
- };
3122
+ // src/components/PriceHistory.tsx
3123
+ import { Box as Box8, Text as Text8 } from "ink";
3124
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3125
+ var LABEL_WIDTH2 = 18;
3126
+ var Row2 = ({
3127
+ label,
3128
+ children
3129
+ }) => /* @__PURE__ */ jsxs8(Box8, { children: [
3130
+ /* @__PURE__ */ jsx9(Box8, { width: LABEL_WIDTH2, flexShrink: 0, children: /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: label }) }),
3131
+ /* @__PURE__ */ jsx9(Text8, { children })
3132
+ ] });
3133
+ var PriceHistory = ({
3134
+ coin,
3135
+ coinType,
3136
+ interval,
3137
+ high,
3138
+ low,
3139
+ change,
3140
+ sparklineText,
3141
+ compact = false
3142
+ }) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingLeft: 1, children: [
3143
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
3144
+ !compact && /* @__PURE__ */ jsx9(Row2, { label: "Coin", children: coin }),
3145
+ !compact && /* @__PURE__ */ jsx9(Row2, { label: "Type", children: coinType }),
3146
+ /* @__PURE__ */ jsx9(Row2, { label: "Interval", children: interval }),
3147
+ /* @__PURE__ */ jsx9(Row2, { label: "High", children: high }),
3148
+ /* @__PURE__ */ jsx9(Row2, { label: "Low", children: low }),
3149
+ /* @__PURE__ */ jsx9(Row2, { label: "Change", children: /* @__PURE__ */ jsx9(Text8, { color: change.color, children: change.text }) })
3150
+ ] }),
3151
+ sparklineText.length > 0 && /* @__PURE__ */ jsx9(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx9(Text8, { children: sparklineText }) }),
3152
+ /* @__PURE__ */ jsx9(Box8, { marginBottom: 1 })
3153
+ ] });
3154
+
3155
+ // src/components/CoinView.tsx
3156
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
3157
+ import { Box as Box11, Text as Text11, useInput as useInput3, useApp as useApp3 } from "ink";
3158
+ import Spinner3 from "ink-spinner";
3159
+
3160
+ // src/components/CoinHoldersView.tsx
3161
+ import { Box as Box9, Text as Text9 } from "ink";
3162
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3163
+ function formatPct(pct) {
3164
+ if (pct < 0.01) return "<0.01%";
3165
+ return `${pct.toFixed(1)}%`;
2854
3166
  }
2855
- function outputCoin(json, coin) {
2856
- outputData(json, {
2857
- json: formatCoinJson(coin),
2858
- render: () => {
2859
- renderOnce(/* @__PURE__ */ jsx7(CoinDetail, { coin }));
3167
+ var makeHolderColumns = (ctx) => [
3168
+ { header: "#", width: 5, accessor: (r) => String(r.rank) },
3169
+ {
3170
+ header: "Holder",
3171
+ width: 20,
3172
+ accessor: (r) => r.ownerProfile?.handle ?? r.ownerAddress
3173
+ },
3174
+ {
3175
+ header: "Balance",
3176
+ width: 18,
3177
+ accessor: (r) => formatBalance(r.balance)
3178
+ },
3179
+ {
3180
+ header: "% Supply",
3181
+ width: 10,
3182
+ accessor: (r) => {
3183
+ if (ctx.totalSupplyNum <= 0) return "-";
3184
+ const balanceNum = parseRawBalance(r.balance);
3185
+ return formatPct(balanceNum / ctx.totalSupplyNum * 100);
2860
3186
  }
2861
- });
2862
- }
2863
- var getCommand = new Command5("get").description("Look up a coin by address or name").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
2864
- "[identifier]",
2865
- "Coin address (0x...) or name (when type prefix is given)"
2866
- ).action(async function(typeOrId, identifier) {
2867
- const json = getJson(this);
2868
- let parsed;
2869
- try {
2870
- parsed = parsePositionalCoinArgs(typeOrId, identifier);
2871
- } catch (err) {
2872
- if (err instanceof CoinArgError) {
2873
- outputErrorAndExit(json, err.message, err.suggestion);
2874
- }
2875
- throw err;
2876
3187
  }
2877
- const apiKey = getApiKey();
2878
- if (apiKey) {
2879
- setApiKey4(apiKey);
3188
+ ];
3189
+ var emptyState3 = /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
3190
+ /* @__PURE__ */ jsx10(Text9, { children: "No holders found for this coin." }),
3191
+ /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "Press q to exit" }) })
3192
+ ] });
3193
+ var CoinHoldersView = ({
3194
+ fetchPage,
3195
+ coinName,
3196
+ totalSupplyNum,
3197
+ limit,
3198
+ autoRefresh,
3199
+ intervalSeconds
3200
+ }) => {
3201
+ const columns = makeHolderColumns({ totalSupplyNum });
3202
+ return /* @__PURE__ */ jsx10(
3203
+ PaginatedTableView,
3204
+ {
3205
+ fetchPage,
3206
+ columns,
3207
+ title: `Top holders \xB7 ${coinName}`,
3208
+ loadingText: "Loading holders\u2026",
3209
+ emptyState: emptyState3,
3210
+ getAddress: (holder) => holder.ownerAddress,
3211
+ limit,
3212
+ autoRefresh,
3213
+ intervalSeconds
3214
+ }
3215
+ );
3216
+ };
3217
+
3218
+ // src/components/CoinTradesView.tsx
3219
+ import { Box as Box10, Text as Text10 } from "ink";
3220
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3221
+ function formatTradeUsd(priceUsdc) {
3222
+ if (!priceUsdc) return "-";
3223
+ const value = Number(priceUsdc);
3224
+ if (value === 0) return "$0.00";
3225
+ return new Intl.NumberFormat("en-US", {
3226
+ style: "currency",
3227
+ currency: "USD",
3228
+ minimumFractionDigits: 2,
3229
+ maximumFractionDigits: 2
3230
+ }).format(value);
3231
+ }
3232
+ var coinTradeColumns = [
3233
+ { header: "#", width: 4, accessor: (t) => String(t.rank) },
3234
+ {
3235
+ header: "Type",
3236
+ width: 6,
3237
+ accessor: (t) => t.activityType ?? "?",
3238
+ color: (t) => t.activityType === "BUY" ? "green" : t.activityType === "SELL" ? "red" : void 0
3239
+ },
3240
+ {
3241
+ header: "User",
3242
+ width: 18,
3243
+ accessor: (t) => t.senderProfile?.handle ?? truncateAddress(t.senderAddress)
3244
+ },
3245
+ {
3246
+ header: "Amount",
3247
+ width: 22,
3248
+ accessor: (t) => {
3249
+ const prefix = t.activityType === "BUY" ? "+" : "-";
3250
+ return `${prefix}${formatCoinsDisplay(t.coinAmount)} coins`;
3251
+ }
3252
+ },
3253
+ {
3254
+ header: "Value",
3255
+ width: 12,
3256
+ accessor: (t) => formatTradeUsd(t.currencyAmountWithPrice.priceUsdc)
3257
+ },
3258
+ {
3259
+ header: "When",
3260
+ width: 14,
3261
+ accessor: (t) => {
3262
+ if (!t.blockTimestamp) return "-";
3263
+ const date = new Date(t.blockTimestamp);
3264
+ if (isNaN(date.getTime())) return "-";
3265
+ return formatRelativeTime(date);
3266
+ }
2880
3267
  }
2881
- if (parsed.kind === "ambiguous-name") {
2882
- let ambResult;
3268
+ ];
3269
+ var emptyState4 = /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
3270
+ /* @__PURE__ */ jsx11(Text10, { children: "No trades found for this coin." }),
3271
+ /* @__PURE__ */ jsx11(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "Press q to exit" }) })
3272
+ ] });
3273
+ var CoinTradesView = ({
3274
+ fetchPage,
3275
+ coinName,
3276
+ limit,
3277
+ autoRefresh,
3278
+ intervalSeconds
3279
+ }) => {
3280
+ return /* @__PURE__ */ jsx11(
3281
+ PaginatedTableView,
3282
+ {
3283
+ fetchPage,
3284
+ columns: coinTradeColumns,
3285
+ title: `Recent trades \xB7 ${coinName}`,
3286
+ loadingText: "Loading trades\u2026",
3287
+ emptyState: emptyState4,
3288
+ getAddress: (trade) => trade.senderAddress,
3289
+ limit,
3290
+ autoRefresh,
3291
+ intervalSeconds
3292
+ }
3293
+ );
3294
+ };
3295
+
3296
+ // src/components/CoinView.tsx
3297
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3298
+ var TAB_NAMES = ["Price History", "Trades", "Holders"];
3299
+ var TRADE_TAB_COLUMNS = coinTradeColumns.filter(
3300
+ (col) => col.header !== "#"
3301
+ );
3302
+ var CoinView = ({
3303
+ fetchData,
3304
+ initialData,
3305
+ autoRefresh = false,
3306
+ intervalSeconds = 30
3307
+ }) => {
3308
+ const { exit } = useApp3();
3309
+ const [activeTab, setActiveTab] = useState4(0);
3310
+ const [loading, setLoading] = useState4(!initialData);
3311
+ const [isRefreshing, setIsRefreshing] = useState4(false);
3312
+ const [error, setError] = useState4(null);
3313
+ const [data, setData] = useState4(initialData ?? null);
3314
+ const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
3315
+ const [manualRefreshCount, setManualRefreshCount] = useState4(0);
3316
+ const hasLoadedOnce = useRef3(!!initialData);
3317
+ const load = useCallback4(async () => {
3318
+ if (hasLoadedOnce.current) {
3319
+ setIsRefreshing(true);
3320
+ } else {
3321
+ setLoading(true);
3322
+ }
3323
+ setError(null);
2883
3324
  try {
2884
- ambResult = await resolveAmbiguousName(parsed.name);
3325
+ const result = await fetchData();
3326
+ setData(result);
3327
+ hasLoadedOnce.current = true;
2885
3328
  } catch (err) {
2886
- outputErrorAndExit(
2887
- json,
2888
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
2889
- );
2890
- return;
3329
+ setError(err instanceof Error ? err.message : String(err));
2891
3330
  }
2892
- if (ambResult.kind === "not-found") {
2893
- outputErrorAndExit(json, ambResult.message);
3331
+ setLoading(false);
3332
+ setIsRefreshing(false);
3333
+ }, [fetchData]);
3334
+ useEffect4(() => {
3335
+ if (initialData && refreshCount === 0 && manualRefreshCount === 0) return;
3336
+ load();
3337
+ }, [load, refreshCount, manualRefreshCount]);
3338
+ useInput3((input, key) => {
3339
+ if (input === "q" || key.escape) {
3340
+ exit();
2894
3341
  return;
2895
3342
  }
2896
- if (ambResult.kind === "ambiguous") {
2897
- if (json) {
2898
- outputData(json, {
2899
- json: {
2900
- matches: [
2901
- { type: "creator-coin", ...formatCoinJson(ambResult.creator) },
2902
- { type: "trend", ...formatCoinJson(ambResult.trend) }
2903
- ],
2904
- hint: `Use: zora get creator-coin ${parsed.name} or zora get trend ${parsed.name}`
2905
- },
2906
- render: () => {
3343
+ if (input === "r" && !loading) {
3344
+ triggerManualRefresh();
3345
+ setManualRefreshCount((c) => c + 1);
3346
+ }
3347
+ if (key.leftArrow) {
3348
+ setActiveTab((t) => Math.max(0, t - 1));
3349
+ }
3350
+ if (key.rightArrow) {
3351
+ setActiveTab((t) => Math.min(2, t + 1));
3352
+ }
3353
+ if (input === "1") setActiveTab(0);
3354
+ if (input === "2") setActiveTab(1);
3355
+ if (input === "3") setActiveTab(2);
3356
+ });
3357
+ if (error && !data) {
3358
+ return /* @__PURE__ */ jsxs11(
3359
+ Box11,
3360
+ {
3361
+ flexDirection: "column",
3362
+ paddingLeft: 1,
3363
+ paddingTop: 1,
3364
+ paddingBottom: 1,
3365
+ children: [
3366
+ /* @__PURE__ */ jsxs11(Text11, { color: "red", children: [
3367
+ "Error: ",
3368
+ error
3369
+ ] }),
3370
+ /* @__PURE__ */ jsx12(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "Press q to exit" }) })
3371
+ ]
3372
+ }
3373
+ );
3374
+ }
3375
+ if (loading && !data) {
3376
+ return /* @__PURE__ */ jsx12(Box11, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { children: [
3377
+ /* @__PURE__ */ jsx12(Spinner3, { type: "dots" }),
3378
+ " Loading coin\u2026"
3379
+ ] }) });
3380
+ }
3381
+ if (!data) return null;
3382
+ const hints = [];
3383
+ if (TAB_NAMES.length > 1) {
3384
+ hints.push("\u2190 \u2192 switch tab");
3385
+ }
3386
+ hints.push(autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh");
3387
+ hints.push("q quit");
3388
+ const footer = hints.join(" \xB7 ");
3389
+ const renderTab = () => {
3390
+ switch (activeTab) {
3391
+ case 0:
3392
+ return data.priceHistory ? /* @__PURE__ */ jsx12(
3393
+ PriceHistory,
3394
+ {
3395
+ coin: data.coin.name,
3396
+ coinType: data.coin.coinType,
3397
+ interval: data.priceHistory.interval,
3398
+ high: data.priceHistory.high,
3399
+ low: data.priceHistory.low,
3400
+ change: data.priceHistory.change,
3401
+ sparklineText: data.priceHistory.sparklineText,
3402
+ compact: true
3403
+ }
3404
+ ) : /* @__PURE__ */ jsx12(
3405
+ Box11,
3406
+ {
3407
+ flexDirection: "column",
3408
+ paddingLeft: 1,
3409
+ paddingTop: 1,
3410
+ paddingBottom: 1,
3411
+ children: /* @__PURE__ */ jsx12(Text11, { children: "No price data available." })
3412
+ }
3413
+ );
3414
+ case 1:
3415
+ return data.trades.length > 0 ? /* @__PURE__ */ jsx12(Table, { columns: TRADE_TAB_COLUMNS, data: data.trades }) : /* @__PURE__ */ jsx12(
3416
+ Box11,
3417
+ {
3418
+ flexDirection: "column",
3419
+ paddingLeft: 1,
3420
+ paddingTop: 1,
3421
+ paddingBottom: 1,
3422
+ children: /* @__PURE__ */ jsx12(Text11, { children: "No trades found." })
3423
+ }
3424
+ );
3425
+ case 2: {
3426
+ const holders = data.holders;
3427
+ if (!holders || holders.error) {
3428
+ return /* @__PURE__ */ jsx12(
3429
+ Box11,
3430
+ {
3431
+ flexDirection: "column",
3432
+ paddingLeft: 1,
3433
+ paddingTop: 1,
3434
+ paddingBottom: 1,
3435
+ children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: holders?.error ? `Could not load holders: ${holders.error}` : "Holder data not available." })
3436
+ }
3437
+ );
3438
+ }
3439
+ if (holders.holders.length === 0) {
3440
+ return /* @__PURE__ */ jsx12(
3441
+ Box11,
3442
+ {
3443
+ flexDirection: "column",
3444
+ paddingLeft: 1,
3445
+ paddingTop: 1,
3446
+ paddingBottom: 1,
3447
+ children: /* @__PURE__ */ jsx12(Text11, { children: "No holders found for this coin." })
3448
+ }
3449
+ );
3450
+ }
3451
+ const totalSupplyNum = Number(data.coin.totalSupply);
3452
+ const columns = makeHolderColumns({ totalSupplyNum });
3453
+ const rankedHolders = holders.holders.map((h, i) => ({
3454
+ ...h,
3455
+ rank: i + 1
3456
+ }));
3457
+ return /* @__PURE__ */ jsx12(
3458
+ Table,
3459
+ {
3460
+ columns,
3461
+ data: rankedHolders,
3462
+ title: "Holders",
3463
+ subtitle: `${rankedHolders.length} of ${holders.totalCount}`
2907
3464
  }
2908
- });
2909
- } else {
2910
- outputCoin(false, ambResult.creator);
2911
- console.log("");
2912
- outputCoin(false, ambResult.trend);
2913
- console.log(
2914
- `
2915
- \x1B[2mUse \`zora get creator-coin ${parsed.name}\` or \`zora get trend ${parsed.name}\` for a specific type.\x1B[0m`
2916
3465
  );
2917
3466
  }
2918
- track("cli_get", {
2919
- lookup_type: "name",
2920
- found: true,
2921
- ambiguous: true,
2922
- output_format: json ? "json" : "text"
2923
- });
2924
- return;
2925
3467
  }
2926
- outputCoin(json, ambResult.coin);
2927
- track("cli_get", {
2928
- lookup_type: "name",
2929
- found: true,
2930
- coin_type: ambResult.coin.coinType,
2931
- output_format: json ? "json" : "text"
2932
- });
2933
- return;
2934
- }
2935
- const ref = coinArgsToRef(parsed);
2936
- let result;
2937
- try {
2938
- result = await resolveCoin(ref);
2939
- } catch (err) {
2940
- outputErrorAndExit(json, `Request failed: ${apiErrorMessage(err)}`);
2941
- return;
2942
- }
2943
- if (result.kind === "not-found") {
2944
- outputErrorAndExit(json, result.message);
2945
- return;
3468
+ };
3469
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
3470
+ isRefreshing && /* @__PURE__ */ jsx12(Box11, { paddingLeft: 1, children: /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3471
+ /* @__PURE__ */ jsx12(Spinner3, { type: "dots" }),
3472
+ " Refreshing\u2026"
3473
+ ] }) }),
3474
+ error && data && /* @__PURE__ */ jsx12(Box11, { paddingLeft: 1, children: /* @__PURE__ */ jsxs11(Text11, { color: "yellow", children: [
3475
+ "\u26A0 Refresh failed: ",
3476
+ error
3477
+ ] }) }),
3478
+ /* @__PURE__ */ jsx12(CoinDetail, { coin: data.coin }),
3479
+ /* @__PURE__ */ jsx12(Box11, { paddingLeft: 1, gap: 2, children: TAB_NAMES.map((name, i) => /* @__PURE__ */ jsx12(Text11, { bold: activeTab === i, dimColor: activeTab !== i, children: activeTab === i ? `[${name}]` : name }, name)) }),
3480
+ renderTab(),
3481
+ /* @__PURE__ */ jsx12(Box11, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: footer }) })
3482
+ ] });
3483
+ };
3484
+
3485
+ // src/lib/price-history.ts
3486
+ import { apiGet } from "@zoralabs/coins-sdk";
3487
+ var VALID_INTERVALS = ["1h", "24h", "1w", "1m", "ALL"];
3488
+ var INTERVAL_TO_API_FIELD = {
3489
+ "1h": "oneHour",
3490
+ "24h": "oneDay",
3491
+ "1w": "oneWeek",
3492
+ "1m": "oneMonth",
3493
+ ALL: "all"
3494
+ };
3495
+ var formatPrice = (price) => {
3496
+ if (price >= 1) {
3497
+ return `$${price.toFixed(2)}`;
2946
3498
  }
2947
- if (result.coin.platformBlocked) {
2948
- outputErrorAndExit(json, bannedCoinMessage(result.coin.address));
2949
- return;
3499
+ if (price >= 0.01) {
3500
+ return `$${price.toFixed(4)}`;
2950
3501
  }
2951
- outputCoin(json, result.coin);
2952
- track("cli_get", {
2953
- lookup_type: typeOrId.startsWith("0x") ? "address" : "name",
2954
- coin_type_filter: parsed.kind === "typed" ? parsed.type : null,
2955
- found: true,
2956
- coin_type: result.coin.coinType,
2957
- output_format: json ? "json" : "text"
3502
+ return `$${price.toPrecision(4)}`;
3503
+ };
3504
+ var formatChange = (first, last) => {
3505
+ if (first === 0) return { text: "-", color: void 0 };
3506
+ const pct = (last - first) / first * 100;
3507
+ const prefix = pct >= 0 ? "+" : "";
3508
+ const text = `${prefix}${pct.toFixed(1)}%`;
3509
+ const color = pct > 0 ? "green" : pct < 0 ? "red" : void 0;
3510
+ return { text, color };
3511
+ };
3512
+ var fetchPriceHistory = async (address, interval) => {
3513
+ const response = await apiGet("/coinPriceHistory", {
3514
+ address
2958
3515
  });
2959
- });
2960
-
2961
- // src/commands/price-history.tsx
2962
- import { Command as Command6 } from "commander";
2963
- import { setApiKey as setApiKey5, apiGet } from "@zoralabs/coins-sdk";
2964
-
2965
- // src/components/PriceHistory.tsx
2966
- import { Box as Box6, Text as Text6 } from "ink";
2967
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
2968
- var LABEL_WIDTH2 = 18;
2969
- var Row2 = ({
2970
- label,
2971
- children
2972
- }) => /* @__PURE__ */ jsxs6(Box6, { children: [
2973
- /* @__PURE__ */ jsx8(Box6, { width: LABEL_WIDTH2, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: label }) }),
2974
- /* @__PURE__ */ jsx8(Text6, { children })
2975
- ] });
2976
- var PriceHistory = ({
2977
- coin,
2978
- coinType,
2979
- interval,
2980
- high,
2981
- low,
2982
- change,
2983
- sparklineText
2984
- }) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 1, children: [
2985
- /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
2986
- /* @__PURE__ */ jsx8(Row2, { label: "Coin", children: coin }),
2987
- /* @__PURE__ */ jsx8(Row2, { label: "Type", children: coinType }),
2988
- /* @__PURE__ */ jsx8(Row2, { label: "Interval", children: interval }),
2989
- /* @__PURE__ */ jsx8(Row2, { label: "High", children: high }),
2990
- /* @__PURE__ */ jsx8(Row2, { label: "Low", children: low }),
2991
- /* @__PURE__ */ jsx8(Row2, { label: "Change", children: /* @__PURE__ */ jsx8(Text6, { color: change.color, children: change.text }) })
2992
- ] }),
2993
- sparklineText.length > 0 && /* @__PURE__ */ jsx8(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text6, { children: sparklineText }) }),
2994
- /* @__PURE__ */ jsx8(Box6, { marginBottom: 1 })
2995
- ] });
3516
+ const data = response.data;
3517
+ const token = data?.zora20Token;
3518
+ if (!token) return [];
3519
+ const field = INTERVAL_TO_API_FIELD[interval];
3520
+ const points = token[field];
3521
+ if (!points || points.length === 0) return [];
3522
+ return points.map((p) => ({
3523
+ timestamp: p.timestamp,
3524
+ price: Number(p.closePrice)
3525
+ }));
3526
+ };
2996
3527
 
2997
3528
  // src/lib/sparkline.ts
2998
3529
  var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
@@ -3024,49 +3555,315 @@ var downsample = (values, maxWidth) => {
3024
3555
  return result;
3025
3556
  };
3026
3557
 
3027
- // src/commands/price-history.tsx
3028
- import { jsx as jsx9 } from "react/jsx-runtime";
3029
- var VALID_INTERVALS = ["1h", "24h", "1w", "1m", "ALL"];
3030
- var INTERVAL_TO_API_FIELD = {
3031
- "1h": "oneHour",
3032
- "24h": "oneDay",
3033
- "1w": "oneWeek",
3034
- "1m": "oneMonth",
3035
- ALL: "all"
3558
+ // src/commands/get.tsx
3559
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3560
+ function formatCoinJson(coin) {
3561
+ return {
3562
+ name: coin.name,
3563
+ address: coin.address,
3564
+ coinType: coin.coinType,
3565
+ marketCap: coin.marketCap,
3566
+ marketCapDelta24h: coin.marketCapDelta24h,
3567
+ volume24h: coin.volume24h,
3568
+ uniqueHolders: coin.uniqueHolders,
3569
+ createdAt: coin.createdAt ?? null,
3570
+ creatorAddress: coin.creatorAddress ?? null,
3571
+ creatorHandle: coin.creatorHandle ?? null
3572
+ };
3573
+ }
3574
+ var resolveApiKey = () => {
3575
+ const apiKey = getApiKey();
3576
+ if (apiKey) {
3577
+ setApiKey4(apiKey);
3578
+ }
3036
3579
  };
3037
- var formatPrice = (price) => {
3038
- if (price >= 1) {
3039
- return `$${price.toFixed(2)}`;
3580
+ var CoinResolutionError = class extends Error {
3581
+ suggestion;
3582
+ constructor(message, suggestion) {
3583
+ super(message);
3584
+ this.suggestion = suggestion;
3585
+ }
3586
+ };
3587
+ async function resolveCoinOrThrow(typeOrId, identifier, command) {
3588
+ const parsed = parsePositionalCoinArgs(typeOrId, identifier);
3589
+ resolveApiKey();
3590
+ if (parsed.kind === "ambiguous-name") {
3591
+ const ambResult = await resolveAmbiguousName(parsed.name);
3592
+ if (ambResult.kind === "not-found") {
3593
+ throw new Error(ambResult.message);
3594
+ }
3595
+ if (ambResult.kind === "ambiguous") {
3596
+ const { message, suggestion } = formatAmbiguousError(
3597
+ parsed.name,
3598
+ ambResult.creator,
3599
+ ambResult.trend,
3600
+ command
3601
+ );
3602
+ throw new CoinResolutionError(message, suggestion);
3603
+ }
3604
+ return {
3605
+ coin: ambResult.coin,
3606
+ lookupType: "name",
3607
+ coinTypeFilter: null
3608
+ };
3609
+ }
3610
+ const ref = coinArgsToRef(parsed);
3611
+ const result = await resolveCoin(ref);
3612
+ if (result.kind === "not-found") {
3613
+ throw new CoinResolutionError(result.message, result.suggestion);
3614
+ }
3615
+ if (result.coin.platformBlocked) {
3616
+ throw new Error(bannedCoinMessage(result.coin.address));
3617
+ }
3618
+ return {
3619
+ coin: result.coin,
3620
+ lookupType: typeOrId.startsWith("0x") ? "address" : "name",
3621
+ coinTypeFilter: parsed.kind === "typed" ? parsed.type : null
3622
+ };
3623
+ }
3624
+ async function resolveAndValidateCoin(json, typeOrId, identifier, command) {
3625
+ try {
3626
+ return await resolveCoinOrThrow(typeOrId, identifier, command);
3627
+ } catch (err) {
3628
+ if (err instanceof CoinArgError) {
3629
+ outputErrorAndExit(json, err.message, err.suggestion);
3630
+ }
3631
+ if (err instanceof CoinResolutionError) {
3632
+ outputErrorAndExit(json, err.message, err.suggestion);
3633
+ }
3634
+ const msg = err instanceof Error ? err.message : String(err);
3635
+ outputErrorAndExit(json, msg);
3636
+ }
3637
+ }
3638
+ async function buildPriceHistoryData(address, interval) {
3639
+ let prices;
3640
+ try {
3641
+ prices = await fetchPriceHistory(address, interval);
3642
+ } catch {
3643
+ return null;
3644
+ }
3645
+ if (prices.length === 0) return null;
3646
+ const priceValues = prices.map((p) => p.price);
3647
+ const high = Math.max(...priceValues);
3648
+ const low = Math.min(...priceValues);
3649
+ const change = formatChange(
3650
+ priceValues[0],
3651
+ priceValues[priceValues.length - 1]
3652
+ );
3653
+ const sparklineText = sparkline(downsample(priceValues, MAX_SPARKLINE_WIDTH));
3654
+ return {
3655
+ high: formatPrice(high),
3656
+ low: formatPrice(low),
3657
+ change,
3658
+ sparklineText,
3659
+ interval
3660
+ };
3661
+ }
3662
+ async function fetchHoldersPageForCoin(address, count, after) {
3663
+ const result = await getCoinHolders({
3664
+ chainId: BASE_CHAIN_ID,
3665
+ address,
3666
+ count,
3667
+ after
3668
+ });
3669
+ const token = result.data?.zora20Token;
3670
+ if (!token) return { items: [] };
3671
+ const items = token.tokenBalances.edges.map((e) => ({
3672
+ balance: e.node.balance,
3673
+ ownerAddress: e.node.ownerAddress,
3674
+ ownerProfile: e.node.ownerProfile && !e.node.ownerProfile.platformBlocked ? { handle: e.node.ownerProfile.handle } : void 0
3675
+ }));
3676
+ return {
3677
+ items,
3678
+ count: token.tokenBalances.count,
3679
+ pageInfo: token.tokenBalances.pageInfo
3680
+ };
3681
+ }
3682
+ async function buildHoldersData(address) {
3683
+ try {
3684
+ const result = await fetchHoldersPageForCoin(address, 10);
3685
+ return {
3686
+ holders: result.items,
3687
+ totalCount: result.count ?? result.items.length
3688
+ };
3689
+ } catch (err) {
3690
+ return {
3691
+ holders: [],
3692
+ totalCount: 0,
3693
+ error: err instanceof Error ? err.message : String(err)
3694
+ };
3695
+ }
3696
+ }
3697
+ async function fetchRecentTrades(address) {
3698
+ try {
3699
+ const response = await getCoinSwaps({ address, first: 10 });
3700
+ const edges = response.data?.zora20Token?.swapActivities?.edges ?? [];
3701
+ return edges.map((e) => e.node);
3702
+ } catch {
3703
+ return [];
3704
+ }
3705
+ }
3706
+ var getCommand = new Command5("get").description("Look up a coin by address or name").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
3707
+ "[identifier]",
3708
+ "Coin address (0x...) or name (when type prefix is given)"
3709
+ ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
3710
+ "--refresh <seconds>",
3711
+ "Auto-refresh interval in seconds, requires --live (min 5)",
3712
+ "30"
3713
+ ).action(async function(typeOrId, identifier) {
3714
+ const output = getOutputMode(this, "live");
3715
+ const json = output === "json";
3716
+ const interval = "1w";
3717
+ const { coin, lookupType, coinTypeFilter } = await resolveAndValidateCoin(
3718
+ json,
3719
+ typeOrId,
3720
+ identifier,
3721
+ "get"
3722
+ );
3723
+ if (json) {
3724
+ const [prices, trades] = await Promise.all([
3725
+ fetchPriceHistory(coin.address, interval).catch(() => []),
3726
+ fetchRecentTrades(coin.address)
3727
+ ]);
3728
+ outputData(json, {
3729
+ json: {
3730
+ ...formatCoinJson(coin),
3731
+ priceHistory: prices.length > 0 ? {
3732
+ interval,
3733
+ high: Math.max(...prices.map((p) => p.price)),
3734
+ low: Math.min(...prices.map((p) => p.price)),
3735
+ change: prices[0].price === 0 ? null : (prices[prices.length - 1].price - prices[0].price) / prices[0].price,
3736
+ prices: prices.map((p) => ({
3737
+ timestamp: p.timestamp,
3738
+ price: p.price
3739
+ }))
3740
+ } : null,
3741
+ trades: trades.map((t) => ({
3742
+ type: t.activityType ?? null,
3743
+ sender: t.senderAddress,
3744
+ senderHandle: t.senderProfile?.handle ?? null,
3745
+ coinAmount: t.coinAmount,
3746
+ valueUsd: t.currencyAmountWithPrice.priceUsdc ?? null,
3747
+ timestamp: t.blockTimestamp,
3748
+ transactionHash: t.transactionHash
3749
+ }))
3750
+ },
3751
+ render: () => {
3752
+ }
3753
+ });
3754
+ track("cli_get", {
3755
+ lookup_type: lookupType,
3756
+ coin_type_filter: coinTypeFilter,
3757
+ coin_type: coin.coinType,
3758
+ output_format: "json"
3759
+ });
3760
+ return;
3761
+ }
3762
+ const { live, intervalSeconds } = getLiveConfig(this, output);
3763
+ if (live) {
3764
+ const [initialPriceHistory, initialTrades, initialHolders] = await Promise.all([
3765
+ buildPriceHistoryData(coin.address, interval),
3766
+ fetchRecentTrades(coin.address),
3767
+ buildHoldersData(coin.address)
3768
+ ]);
3769
+ const fetchData = async () => {
3770
+ const { coin: freshCoin } = await resolveCoinOrThrow(
3771
+ typeOrId,
3772
+ identifier,
3773
+ "get"
3774
+ );
3775
+ const [priceHistory, trades, holders] = await Promise.all([
3776
+ buildPriceHistoryData(freshCoin.address, interval),
3777
+ fetchRecentTrades(freshCoin.address),
3778
+ buildHoldersData(freshCoin.address)
3779
+ ]);
3780
+ return { coin: freshCoin, priceHistory, trades, holders };
3781
+ };
3782
+ await renderLive(
3783
+ /* @__PURE__ */ jsx13(
3784
+ CoinView,
3785
+ {
3786
+ fetchData,
3787
+ initialData: {
3788
+ coin,
3789
+ priceHistory: initialPriceHistory,
3790
+ trades: initialTrades,
3791
+ holders: initialHolders
3792
+ },
3793
+ autoRefresh: live,
3794
+ intervalSeconds
3795
+ }
3796
+ )
3797
+ );
3798
+ track("cli_get", {
3799
+ lookup_type: lookupType,
3800
+ coin_type_filter: coinTypeFilter,
3801
+ coin_type: coin.coinType,
3802
+ output_format: "live",
3803
+ interval: intervalSeconds
3804
+ });
3805
+ } else {
3806
+ const [priceHistory, trades] = await Promise.all([
3807
+ buildPriceHistoryData(coin.address, interval),
3808
+ fetchRecentTrades(coin.address)
3809
+ ]);
3810
+ renderOnce(
3811
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
3812
+ /* @__PURE__ */ jsx13(CoinDetail, { coin }),
3813
+ priceHistory ? /* @__PURE__ */ jsx13(
3814
+ PriceHistory,
3815
+ {
3816
+ coin: coin.name,
3817
+ coinType: coin.coinType,
3818
+ interval: priceHistory.interval,
3819
+ high: priceHistory.high,
3820
+ low: priceHistory.low,
3821
+ change: priceHistory.change,
3822
+ sparklineText: priceHistory.sparklineText,
3823
+ compact: true
3824
+ }
3825
+ ) : /* @__PURE__ */ jsx13(Box12, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx13(Text12, { dimColor: true, children: "No price data available." }) }),
3826
+ trades.length > 0 ? /* @__PURE__ */ jsx13(
3827
+ Table,
3828
+ {
3829
+ columns: coinTradeColumns.filter((c) => c.header !== "#"),
3830
+ data: trades,
3831
+ title: "Recent Trades"
3832
+ }
3833
+ ) : /* @__PURE__ */ jsx13(Box12, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx13(Text12, { dimColor: true, children: "No trades found." }) })
3834
+ ] })
3835
+ );
3836
+ track("cli_get", {
3837
+ lookup_type: lookupType,
3838
+ coin_type_filter: coinTypeFilter,
3839
+ coin_type: coin.coinType,
3840
+ output_format: "static"
3841
+ });
3040
3842
  }
3041
- if (price >= 0.01) {
3042
- return `$${price.toFixed(4)}`;
3843
+ });
3844
+ getCommand.command("price-history").description("Display price history for a coin").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
3845
+ "[identifier]",
3846
+ "Coin address (0x...) or name (when type prefix is given)"
3847
+ ).option(
3848
+ "--interval <interval>",
3849
+ `Time range: ${VALID_INTERVALS.join(", ")}`,
3850
+ "1w"
3851
+ ).action(async function(typeOrId, identifier, opts) {
3852
+ const json = getJson(this);
3853
+ const interval = opts.interval ?? "1w";
3854
+ if (!VALID_INTERVALS.includes(interval)) {
3855
+ outputErrorAndExit(
3856
+ json,
3857
+ `Invalid --interval value: ${interval}.`,
3858
+ `Supported: ${VALID_INTERVALS.join(", ")}`
3859
+ );
3043
3860
  }
3044
- return `$${price.toPrecision(4)}`;
3045
- };
3046
- var formatChange = (first, last) => {
3047
- if (first === 0) return { text: "-", color: void 0 };
3048
- const pct = (last - first) / first * 100;
3049
- const prefix = pct >= 0 ? "+" : "";
3050
- const text = `${prefix}${pct.toFixed(1)}%`;
3051
- const color = pct > 0 ? "green" : pct < 0 ? "red" : void 0;
3052
- return { text, color };
3053
- };
3054
- var fetchPriceHistory = async (address, interval) => {
3055
- const response = await apiGet("/coinPriceHistory", {
3056
- address
3057
- });
3058
- const data = response.data;
3059
- const token = data?.zora20Token;
3060
- if (!token) return [];
3061
- const field = INTERVAL_TO_API_FIELD[interval];
3062
- const points = token[field];
3063
- if (!points || points.length === 0) return [];
3064
- return points.map((p) => ({
3065
- timestamp: p.timestamp,
3066
- price: Number(p.closePrice)
3067
- }));
3068
- };
3069
- async function showPriceHistory(json, coin, interval) {
3861
+ const { coin, lookupType } = await resolveAndValidateCoin(
3862
+ json,
3863
+ typeOrId,
3864
+ identifier,
3865
+ "get price-history"
3866
+ );
3070
3867
  let prices;
3071
3868
  try {
3072
3869
  prices = await fetchPriceHistory(coin.address, interval);
@@ -3075,7 +3872,6 @@ async function showPriceHistory(json, coin, interval) {
3075
3872
  json,
3076
3873
  `Failed to fetch price data: ${err instanceof Error ? err.message : String(err)}`
3077
3874
  );
3078
- return;
3079
3875
  }
3080
3876
  if (prices.length === 0) {
3081
3877
  outputErrorAndExit(
@@ -3083,7 +3879,6 @@ async function showPriceHistory(json, coin, interval) {
3083
3879
  `No price data found for ${coin.name} in the last ${interval}.`,
3084
3880
  "Try a longer interval with --interval"
3085
3881
  );
3086
- return;
3087
3882
  }
3088
3883
  const priceValues = prices.map((p) => p.price);
3089
3884
  const high = Math.max(...priceValues);
@@ -3092,11 +3887,13 @@ async function showPriceHistory(json, coin, interval) {
3092
3887
  priceValues[0],
3093
3888
  priceValues[priceValues.length - 1]
3094
3889
  );
3095
- const sparklineText = sparkline(downsample(priceValues, MAX_SPARKLINE_WIDTH));
3890
+ const sparklineText = sparkline(
3891
+ downsample(priceValues, MAX_SPARKLINE_WIDTH)
3892
+ );
3096
3893
  outputData(json, {
3097
3894
  json: {
3098
3895
  coin: coin.name,
3099
- type: coin.coinType,
3896
+ coinType: coin.coinType,
3100
3897
  interval,
3101
3898
  high,
3102
3899
  low,
@@ -3108,7 +3905,7 @@ async function showPriceHistory(json, coin, interval) {
3108
3905
  },
3109
3906
  render: () => {
3110
3907
  renderOnce(
3111
- /* @__PURE__ */ jsx9(
3908
+ /* @__PURE__ */ jsx13(
3112
3909
  PriceHistory,
3113
3910
  {
3114
3911
  coin: coin.name,
@@ -3123,145 +3920,316 @@ async function showPriceHistory(json, coin, interval) {
3123
3920
  );
3124
3921
  }
3125
3922
  });
3126
- return prices.length;
3923
+ track("cli_get_price_history", {
3924
+ lookup_type: lookupType,
3925
+ coin_type: coin.coinType,
3926
+ interval,
3927
+ data_points: prices.length,
3928
+ output_format: json ? "json" : "text"
3929
+ });
3930
+ });
3931
+ function formatTradeJson(node) {
3932
+ return {
3933
+ type: node.activityType ?? null,
3934
+ sender: node.senderAddress,
3935
+ senderHandle: node.senderProfile?.handle ?? null,
3936
+ coinAmount: node.coinAmount,
3937
+ valueUsd: node.currencyAmountWithPrice.priceUsdc ?? null,
3938
+ timestamp: node.blockTimestamp,
3939
+ transactionHash: node.transactionHash
3940
+ };
3941
+ }
3942
+ async function fetchTradesPage(address, count, after) {
3943
+ const response = await getCoinSwaps({ address, first: count, after });
3944
+ if (response.error) {
3945
+ throw new Error(extractErrorMessage(response.error));
3946
+ }
3947
+ const swapActivities = response.data?.zora20Token?.swapActivities;
3948
+ const edges = swapActivities?.edges ?? [];
3949
+ const items = edges.map(
3950
+ (e) => e.node
3951
+ );
3952
+ const count_ = swapActivities?.count ?? items.length;
3953
+ const pageInfo = swapActivities?.pageInfo;
3954
+ return { items, count: count_, pageInfo };
3127
3955
  }
3128
- var priceHistoryCommand = new Command6("price-history").description("Display price history for a coin").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
3956
+ getCommand.command("trades").description("Show recent buy/sell activity on a coin").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
3129
3957
  "[identifier]",
3130
3958
  "Coin address (0x...) or name (when type prefix is given)"
3131
- ).option(
3132
- "--interval <interval>",
3133
- `Time range: ${VALID_INTERVALS.join(", ")}`,
3134
- "1w"
3135
- ).action(async function(typeOrId, identifier, opts) {
3136
- const json = getJson(this);
3137
- const interval = opts.interval ?? "1w";
3138
- if (!VALID_INTERVALS.includes(interval)) {
3959
+ ).option("--limit <n>", "Number of results (default 10, max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
3960
+ "--refresh <seconds>",
3961
+ "Auto-refresh interval in seconds, requires --live (min 5)",
3962
+ "30"
3963
+ ).action(async function(typeOrId, identifier) {
3964
+ const output = getOutputMode(this, "live");
3965
+ const json = output === "json";
3966
+ const limit = Math.min(
3967
+ 20,
3968
+ Math.max(1, parseInt(this.opts().limit, 10) || 10)
3969
+ );
3970
+ const after = this.opts().after;
3971
+ if (output === "live" && after) {
3139
3972
  outputErrorAndExit(
3140
- json,
3141
- `Invalid --interval value: ${interval}.`,
3142
- `Supported: ${VALID_INTERVALS.join(", ")}`
3973
+ false,
3974
+ "--after cannot be used in live mode.",
3975
+ "Use --static or --json to paginate with a cursor."
3143
3976
  );
3144
3977
  }
3145
- let parsed;
3146
- try {
3147
- parsed = parsePositionalCoinArgs(typeOrId, identifier);
3148
- } catch (err) {
3149
- if (err instanceof CoinArgError) {
3150
- outputErrorAndExit(json, err.message, err.suggestion);
3978
+ const { coin } = await resolveAndValidateCoin(
3979
+ json,
3980
+ typeOrId,
3981
+ identifier,
3982
+ "get trades"
3983
+ );
3984
+ if (json) {
3985
+ const result = await fetchTradesPage(coin.address, limit, after).catch(
3986
+ (err) => outputErrorAndExit(
3987
+ json,
3988
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
3989
+ )
3990
+ );
3991
+ outputData(json, {
3992
+ json: {
3993
+ coin: { name: coin.name, address: coin.address },
3994
+ trades: result.items.map(formatTradeJson),
3995
+ pageInfo: result.pageInfo ?? null
3996
+ },
3997
+ render: () => {
3998
+ }
3999
+ });
4000
+ track("cli_get_trades", {
4001
+ result_count: result.items.length,
4002
+ output_format: "json"
4003
+ });
4004
+ } else {
4005
+ const { live, intervalSeconds } = getLiveConfig(this, output);
4006
+ if (live) {
4007
+ const fetchPage = (cursor) => fetchTradesPage(coin.address, limit, cursor);
4008
+ await renderLive(
4009
+ /* @__PURE__ */ jsx13(
4010
+ CoinTradesView,
4011
+ {
4012
+ fetchPage,
4013
+ coinName: coin.name,
4014
+ limit,
4015
+ autoRefresh: live,
4016
+ intervalSeconds
4017
+ }
4018
+ )
4019
+ );
4020
+ track("cli_get_trades", {
4021
+ output_format: "live",
4022
+ live,
4023
+ interval: intervalSeconds
4024
+ });
4025
+ } else {
4026
+ const result = await fetchTradesPage(coin.address, limit, after).catch(
4027
+ (err) => outputErrorAndExit(
4028
+ json,
4029
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
4030
+ )
4031
+ );
4032
+ const rankedTrades = result.items.map((t, i) => ({
4033
+ ...t,
4034
+ rank: i + 1
4035
+ }));
4036
+ if (rankedTrades.length === 0) {
4037
+ renderOnce(
4038
+ /* @__PURE__ */ jsx13(
4039
+ Box12,
4040
+ {
4041
+ flexDirection: "column",
4042
+ paddingLeft: 1,
4043
+ paddingTop: 1,
4044
+ paddingBottom: 1,
4045
+ children: /* @__PURE__ */ jsxs12(Text12, { children: [
4046
+ "No trades found for ",
4047
+ coin.name,
4048
+ " (",
4049
+ truncateAddress(coin.address),
4050
+ ")"
4051
+ ] })
4052
+ }
4053
+ )
4054
+ );
4055
+ } else {
4056
+ const footer = result.pageInfo?.hasNextPage && result.pageInfo.endCursor ? `Next page: zora get trades ${typeOrId}${identifier ? " " + identifier : ""} --limit ${limit} --after ${result.pageInfo.endCursor}` : void 0;
4057
+ renderOnce(
4058
+ /* @__PURE__ */ jsx13(
4059
+ Table,
4060
+ {
4061
+ columns: coinTradeColumns,
4062
+ data: rankedTrades,
4063
+ title: `Recent trades \xB7 ${coin.name}`,
4064
+ subtitle: `${rankedTrades.length} of ${result.count}`,
4065
+ footer
4066
+ }
4067
+ )
4068
+ );
4069
+ }
4070
+ track("cli_get_trades", {
4071
+ result_count: result.items.length,
4072
+ output_format: "static"
4073
+ });
3151
4074
  }
3152
- throw err;
3153
4075
  }
3154
- const apiKey = getApiKey();
3155
- if (apiKey) {
3156
- setApiKey5(apiKey);
4076
+ });
4077
+ getCommand.command("holders").description("Show top holders of a coin").argument("[typeOrId]", "Type prefix (creator-coin, trend) or identifier").argument(
4078
+ "[identifier]",
4079
+ "Coin address (0x...) or name (when type prefix is given)"
4080
+ ).option("--limit <n>", "Number of results per page (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
4081
+ "--refresh <seconds>",
4082
+ "Auto-refresh interval in seconds, requires --live (min 5)",
4083
+ "30"
4084
+ ).action(async function(typeOrId, identifier) {
4085
+ const output = getOutputMode(this, "live");
4086
+ const json = output === "json";
4087
+ const opts = this.opts();
4088
+ const limit = parseInt(opts.limit, 10);
4089
+ const after = opts.after;
4090
+ if (isNaN(limit) || limit <= 0 || limit > 20) {
4091
+ outputErrorAndExit(
4092
+ json,
4093
+ `Invalid --limit value: ${opts.limit}. Must be an integer between 1 and 20.`,
4094
+ "Usage: zora get holders --limit 10"
4095
+ );
3157
4096
  }
3158
- if (parsed.kind === "ambiguous-name") {
3159
- let ambResult;
4097
+ if (output === "live" && after) {
4098
+ outputErrorAndExit(
4099
+ false,
4100
+ "--after cannot be used in live mode.",
4101
+ "Use --static or --json to paginate with a cursor."
4102
+ );
4103
+ }
4104
+ const { coin, lookupType } = await resolveAndValidateCoin(
4105
+ json,
4106
+ typeOrId,
4107
+ identifier,
4108
+ "get holders"
4109
+ );
4110
+ const totalSupply = Number(coin.totalSupply);
4111
+ if (json) {
4112
+ let result;
3160
4113
  try {
3161
- ambResult = await resolveAmbiguousName(parsed.name);
4114
+ result = await fetchHoldersPageForCoin(coin.address, limit, after);
3162
4115
  } catch (err) {
3163
4116
  outputErrorAndExit(
3164
4117
  json,
3165
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
4118
+ `Failed to fetch holders: ${err instanceof Error ? err.message : String(err)}`
3166
4119
  );
3167
- return;
3168
- }
3169
- if (ambResult.kind === "not-found") {
3170
- outputErrorAndExit(json, ambResult.message);
3171
- return;
3172
4120
  }
3173
- if (ambResult.kind === "ambiguous") {
3174
- if (json) {
3175
- const [creatorPrices, trendPrices] = await Promise.all([
3176
- fetchPriceHistory(ambResult.creator.address, interval),
3177
- fetchPriceHistory(ambResult.trend.address, interval)
3178
- ]);
3179
- outputData(json, {
3180
- json: {
3181
- matches: [
3182
- {
3183
- type: "creator-coin",
3184
- coin: ambResult.creator.name,
3185
- prices: creatorPrices
3186
- },
3187
- {
3188
- type: "trend",
3189
- coin: ambResult.trend.name,
3190
- prices: trendPrices
3191
- }
3192
- ],
3193
- hint: `Use: zora price-history creator-coin ${parsed.name} or zora price-history trend ${parsed.name}`
3194
- },
3195
- render: () => {
4121
+ outputData(json, {
4122
+ json: {
4123
+ coin: coin.name,
4124
+ address: coin.address,
4125
+ coinType: coin.coinType,
4126
+ totalHolders: result.count ?? 0,
4127
+ holders: result.items.map((h, i) => ({
4128
+ rank: i + 1,
4129
+ handle: h.ownerProfile?.handle ?? h.ownerAddress,
4130
+ address: h.ownerAddress,
4131
+ balance: formatBalance(h.balance),
4132
+ balanceRaw: h.balance,
4133
+ ownershipPercent: totalSupply > 0 ? parseRawBalance(h.balance) / totalSupply * 100 : 0
4134
+ })),
4135
+ ...result.pageInfo?.hasNextPage && result.pageInfo.endCursor ? { nextCursor: result.pageInfo.endCursor } : {}
4136
+ },
4137
+ render: () => {
4138
+ }
4139
+ });
4140
+ track("cli_get_holders", {
4141
+ lookup_type: lookupType,
4142
+ coin_type: coin.coinType,
4143
+ limit,
4144
+ total_holders: result.count ?? 0,
4145
+ output_format: "json"
4146
+ });
4147
+ } else {
4148
+ const { live, intervalSeconds } = getLiveConfig(this, output);
4149
+ if (live) {
4150
+ const fetchPage = (cursor) => fetchHoldersPageForCoin(coin.address, limit, cursor);
4151
+ await renderLive(
4152
+ /* @__PURE__ */ jsx13(
4153
+ CoinHoldersView,
4154
+ {
4155
+ fetchPage,
4156
+ coinName: coin.name,
4157
+ totalSupplyNum: totalSupply,
4158
+ limit,
4159
+ autoRefresh: live,
4160
+ intervalSeconds
3196
4161
  }
3197
- });
3198
- } else {
3199
- await showPriceHistory(
3200
- false,
3201
- ambResult.creator,
3202
- interval
4162
+ )
4163
+ );
4164
+ track("cli_get_holders", {
4165
+ lookup_type: lookupType,
4166
+ coin_type: coin.coinType,
4167
+ limit,
4168
+ output_format: "live",
4169
+ interval: intervalSeconds
4170
+ });
4171
+ } else {
4172
+ let result;
4173
+ try {
4174
+ result = await fetchHoldersPageForCoin(coin.address, limit, after);
4175
+ } catch (err) {
4176
+ outputErrorAndExit(
4177
+ json,
4178
+ `Failed to fetch holders: ${err instanceof Error ? err.message : String(err)}`
3203
4179
  );
3204
- console.log("");
3205
- await showPriceHistory(false, ambResult.trend, interval);
3206
- console.log(
3207
- `
3208
- \x1B[2mUse \`zora price-history creator-coin ${parsed.name}\` or \`zora price-history trend ${parsed.name}\` for a specific type.\x1B[0m`
4180
+ }
4181
+ if (result.items.length === 0) {
4182
+ renderOnce(
4183
+ /* @__PURE__ */ jsx13(
4184
+ Box12,
4185
+ {
4186
+ flexDirection: "column",
4187
+ paddingLeft: 1,
4188
+ paddingTop: 1,
4189
+ paddingBottom: 1,
4190
+ children: /* @__PURE__ */ jsxs12(Text12, { children: [
4191
+ "No holders found for ",
4192
+ coin.name,
4193
+ "."
4194
+ ] })
4195
+ }
4196
+ )
4197
+ );
4198
+ } else {
4199
+ const holderColumns = makeHolderColumns({
4200
+ totalSupplyNum: totalSupply
4201
+ });
4202
+ const rankedItems = result.items.map((item, i) => ({
4203
+ ...item,
4204
+ rank: i + 1
4205
+ }));
4206
+ const footer = result.pageInfo?.hasNextPage && result.pageInfo.endCursor ? `Next page: zora get holders ${coin.address} --limit ${limit} --after ${result.pageInfo.endCursor}` : void 0;
4207
+ renderOnce(
4208
+ /* @__PURE__ */ jsx13(
4209
+ Table,
4210
+ {
4211
+ columns: holderColumns,
4212
+ data: rankedItems,
4213
+ title: `Top holders \xB7 ${coin.name}`,
4214
+ subtitle: `${rankedItems.length} of ${result.count ?? rankedItems.length}`,
4215
+ footer
4216
+ }
4217
+ )
3209
4218
  );
3210
4219
  }
3211
- track("cli_price_history", {
3212
- lookup_type: "name",
3213
- ambiguous: true,
3214
- interval,
3215
- output_format: json ? "json" : "text"
4220
+ track("cli_get_holders", {
4221
+ lookup_type: lookupType,
4222
+ coin_type: coin.coinType,
4223
+ limit,
4224
+ total_holders: result.count ?? 0,
4225
+ output_format: "static"
3216
4226
  });
3217
- return;
3218
4227
  }
3219
- const dataPoints2 = await showPriceHistory(
3220
- json,
3221
- ambResult.coin,
3222
- interval
3223
- );
3224
- track("cli_price_history", {
3225
- lookup_type: "name",
3226
- coin_type: ambResult.coin.coinType,
3227
- interval,
3228
- data_points: dataPoints2,
3229
- output_format: json ? "json" : "text"
3230
- });
3231
- return;
3232
- }
3233
- const ref = coinArgsToRef(parsed);
3234
- let result;
3235
- try {
3236
- result = await resolveCoin(ref);
3237
- } catch (err) {
3238
- outputErrorAndExit(
3239
- json,
3240
- `Request failed: ${err instanceof Error ? err.message : String(err)}`
3241
- );
3242
- return;
3243
- }
3244
- if (result.kind === "not-found") {
3245
- outputErrorAndExit(json, result.message, result.suggestion);
3246
- return;
3247
- }
3248
- if (result.coin.platformBlocked) {
3249
- outputErrorAndExit(json, bannedCoinMessage(result.coin.address));
3250
- return;
3251
4228
  }
3252
- const { coin } = result;
3253
- const dataPoints = await showPriceHistory(json, coin, interval);
3254
- track("cli_price_history", {
3255
- lookup_type: typeOrId.startsWith("0x") ? "address" : "name",
3256
- coin_type: coin.coinType,
3257
- interval,
3258
- data_points: dataPoints,
3259
- output_format: json ? "json" : "text"
3260
- });
3261
4229
  });
3262
4230
 
3263
4231
  // src/commands/sell.ts
3264
- import { Command as Command7 } from "commander";
4232
+ import { Command as Command6 } from "commander";
3265
4233
  import confirm3 from "@inquirer/confirm";
3266
4234
  import {
3267
4235
  erc20Abi as erc20Abi3,
@@ -3272,7 +4240,7 @@ import {
3272
4240
  import {
3273
4241
  createTradeCall as createTradeCall2,
3274
4242
  getCoin as getCoin3,
3275
- setApiKey as setApiKey6,
4243
+ setApiKey as setApiKey5,
3276
4244
  tradeCoin as tradeCoin2
3277
4245
  } from "@zoralabs/coins-sdk";
3278
4246
  function printSellQuote(output, info) {
@@ -3349,7 +4317,7 @@ function printSellResult(output, info) {
3349
4317
  console.log(` Tx ${info.txHash}
3350
4318
  `);
3351
4319
  }
3352
- var sellCommand = new Command7("sell").description("Sell a coin").argument(
4320
+ var sellCommand = new Command6("sell").description("Sell a coin").argument(
3353
4321
  "[typeOrId]",
3354
4322
  "Type prefix (creator-coin, trend) or coin address/name"
3355
4323
  ).argument("[identifier]", "Coin name (when type prefix is given)").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(typeOrId, identifier, opts) {
@@ -3366,9 +4334,10 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument(
3366
4334
  }
3367
4335
  const apiKey = getApiKey();
3368
4336
  if (apiKey) {
3369
- setApiKey6(apiKey);
4337
+ setApiKey5(apiKey);
3370
4338
  }
3371
4339
  let coinAddress;
4340
+ let earlyAccount;
3372
4341
  if (parsed.kind === "address") {
3373
4342
  if (!isAddress2(parsed.address)) {
3374
4343
  outputErrorAndExit(json, `Invalid address: ${parsed.address}`);
@@ -3378,8 +4347,23 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument(
3378
4347
  } else if (parsed.kind === "ambiguous-name") {
3379
4348
  let ambResult;
3380
4349
  try {
3381
- ambResult = await resolveAmbiguousName(parsed.name);
4350
+ earlyAccount = resolveAccount(json);
4351
+ const { publicClient: earlyPublicClient } = createClients(earlyAccount);
4352
+ ambResult = await resolveAmbiguousByNameAndBalance(
4353
+ parsed.name,
4354
+ (addr) => earlyPublicClient.readContract({
4355
+ abi: erc20Abi3,
4356
+ address: addr,
4357
+ functionName: "balanceOf",
4358
+ args: [earlyAccount.address]
4359
+ })
4360
+ );
3382
4361
  } catch (err) {
4362
+ if (debug) {
4363
+ console.error(
4364
+ `[debug] resolve failed: ${err instanceof Error ? err.message : String(err)}`
4365
+ );
4366
+ }
3383
4367
  outputErrorAndExit(
3384
4368
  json,
3385
4369
  `Request failed: ${err instanceof Error ? err.message : String(err)}`
@@ -3441,7 +4425,7 @@ var sellCommand = new Command7("sell").description("Sell a coin").argument(
3441
4425
  );
3442
4426
  }
3443
4427
  const slippage = slippagePct / 100;
3444
- const account = resolveAccount(json);
4428
+ const account = earlyAccount ?? resolveAccount(json);
3445
4429
  const { publicClient, walletClient } = createClients(account);
3446
4430
  let token;
3447
4431
  try {
@@ -3582,7 +4566,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3582
4566
  if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
3583
4567
  if (json) {
3584
4568
  outputJson({ error: errorBody ?? msg });
3585
- process.exit(1);
4569
+ safeExit(ERROR);
3586
4570
  }
3587
4571
  outputErrorAndExit(
3588
4572
  json,
@@ -3660,7 +4644,7 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3660
4644
  });
3661
4645
  if (!ok) {
3662
4646
  console.error("Aborted.");
3663
- process.exit(0);
4647
+ safeExit(SUCCESS);
3664
4648
  }
3665
4649
  }
3666
4650
  let receipt;
@@ -3750,24 +4734,28 @@ ${err instanceof Error ? err.stack || err.message : String(err)}
3750
4734
  });
3751
4735
 
3752
4736
  // src/commands/profile.tsx
3753
- import { Command as Command8 } from "commander";
3754
- import { Box as Box8, Text as Text8 } from "ink";
4737
+ import { Command as Command7 } from "commander";
4738
+ import { Box as Box17, Text as Text17 } from "ink";
3755
4739
  import {
3756
4740
  getProfileCoins,
3757
4741
  getProfileBalances as getProfileBalances2,
3758
- setApiKey as setApiKey7
4742
+ getWalletTradeActivity,
4743
+ setApiKey as setApiKey6
3759
4744
  } from "@zoralabs/coins-sdk";
3760
4745
  import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
3761
4746
 
3762
4747
  // src/components/ProfileView.tsx
3763
- import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
3764
- import { Box as Box7, Text as Text7, useInput as useInput3, useApp as useApp3 } from "ink";
3765
- import Spinner3 from "ink-spinner";
3766
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
3767
- var TAB_NAMES = ["Posts", "Holdings"];
4748
+ import { useState as useState5, useEffect as useEffect5, useCallback as useCallback5, useRef as useRef4 } from "react";
4749
+ import { Box as Box15, Text as Text15, useInput as useInput4, useApp as useApp4 } from "ink";
4750
+ import Spinner4 from "ink-spinner";
4751
+
4752
+ // src/components/ProfilePostsView.tsx
4753
+ import { Box as Box13, Text as Text13 } from "ink";
4754
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
3768
4755
  var postColumns = [
3769
4756
  { header: "#", width: 4, accessor: (c) => String(c.rank) },
3770
4757
  { header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
4758
+ { header: "Address", width: 22, accessor: (c) => c.address ?? "" },
3771
4759
  {
3772
4760
  header: "Type",
3773
4761
  width: 14,
@@ -3800,22 +4788,121 @@ var postColumns = [
3800
4788
  }
3801
4789
  }
3802
4790
  ];
4791
+ var emptyState5 = /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
4792
+ /* @__PURE__ */ jsx14(Text13, { children: "No posts found for this profile." }),
4793
+ /* @__PURE__ */ jsx14(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: "Press q to exit" }) })
4794
+ ] });
4795
+ var ProfilePostsView = ({
4796
+ fetchPage,
4797
+ identifier,
4798
+ limit,
4799
+ autoRefresh,
4800
+ intervalSeconds
4801
+ }) => {
4802
+ return /* @__PURE__ */ jsx14(
4803
+ PaginatedTableView,
4804
+ {
4805
+ fetchPage,
4806
+ columns: postColumns,
4807
+ title: `Posts \xB7 ${identifier}`,
4808
+ loadingText: "Loading posts\u2026",
4809
+ emptyState: emptyState5,
4810
+ getAddress: (post) => post.address,
4811
+ limit,
4812
+ autoRefresh,
4813
+ intervalSeconds
4814
+ }
4815
+ );
4816
+ };
4817
+
4818
+ // src/components/ProfileTradesView.tsx
4819
+ import { Box as Box14, Text as Text14 } from "ink";
4820
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
4821
+ var tradeColumns = [
4822
+ { header: "#", width: 4, accessor: (t) => String(t.rank) },
4823
+ {
4824
+ header: "Side",
4825
+ width: 6,
4826
+ accessor: (t) => t.swapActivityType ?? "-",
4827
+ color: (t) => t.swapActivityType === "BUY" ? "green" : t.swapActivityType === "SELL" ? "red" : void 0
4828
+ },
4829
+ {
4830
+ header: "Coin",
4831
+ width: 18,
4832
+ accessor: (t) => t.coin?.name ?? "Unknown"
4833
+ },
4834
+ {
4835
+ header: "Type",
4836
+ width: 10,
4837
+ accessor: (t) => COIN_TYPE_DISPLAY[t.coin?.coinType ?? ""] ?? t.coin?.coinType ?? ""
4838
+ },
4839
+ {
4840
+ header: "Amount",
4841
+ width: 14,
4842
+ accessor: (t) => formatCoinsDisplay(t.coinAmount)
4843
+ },
4844
+ {
4845
+ header: "USD Value",
4846
+ width: 12,
4847
+ accessor: (t) => formatCompactUsd(t.currencyAmountWithPrice.amountUsd)
4848
+ },
4849
+ {
4850
+ header: "When",
4851
+ width: 16,
4852
+ accessor: (t) => {
4853
+ if (!t.blockTimestamp) return "-";
4854
+ const date = new Date(t.blockTimestamp);
4855
+ if (isNaN(date.getTime())) return "-";
4856
+ return formatRelativeTime(date);
4857
+ }
4858
+ }
4859
+ ];
4860
+ var emptyState6 = /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
4861
+ /* @__PURE__ */ jsx15(Text14, { children: "No trades found for this profile." }),
4862
+ /* @__PURE__ */ jsx15(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: "Press q to exit" }) })
4863
+ ] });
4864
+ var ProfileTradesView = ({
4865
+ fetchPage,
4866
+ identifier,
4867
+ limit,
4868
+ autoRefresh,
4869
+ intervalSeconds
4870
+ }) => {
4871
+ return /* @__PURE__ */ jsx15(
4872
+ PaginatedTableView,
4873
+ {
4874
+ fetchPage,
4875
+ columns: tradeColumns,
4876
+ title: `Trades \xB7 ${identifier}`,
4877
+ loadingText: "Loading trades\u2026",
4878
+ emptyState: emptyState6,
4879
+ getAddress: (trade) => trade.coin?.address,
4880
+ limit,
4881
+ autoRefresh,
4882
+ intervalSeconds
4883
+ }
4884
+ );
4885
+ };
4886
+
4887
+ // src/components/ProfileView.tsx
4888
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
4889
+ var TAB_NAMES2 = ["Posts", "Holdings", "Trades"];
3803
4890
  var ProfileView = ({
3804
4891
  fetchData,
3805
4892
  identifier,
3806
4893
  autoRefresh = false,
3807
4894
  intervalSeconds = 30
3808
4895
  }) => {
3809
- const { exit } = useApp3();
3810
- const [activeTab, setActiveTab] = useState4(0);
3811
- const [loading, setLoading] = useState4(true);
3812
- const [isRefreshing, setIsRefreshing] = useState4(false);
3813
- const [error, setError] = useState4(null);
3814
- const [data, setData] = useState4(null);
4896
+ const { exit } = useApp4();
4897
+ const [activeTab, setActiveTab] = useState5(0);
4898
+ const [loading, setLoading] = useState5(true);
4899
+ const [isRefreshing, setIsRefreshing] = useState5(false);
4900
+ const [error, setError] = useState5(null);
4901
+ const [data, setData] = useState5(null);
3815
4902
  const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
3816
- const [manualRefreshCount, setManualRefreshCount] = useState4(0);
3817
- const hasLoadedOnce = useRef3(false);
3818
- const load = useCallback4(async () => {
4903
+ const [manualRefreshCount, setManualRefreshCount] = useState5(0);
4904
+ const hasLoadedOnce = useRef4(false);
4905
+ const load = useCallback5(async () => {
3819
4906
  if (hasLoadedOnce.current) {
3820
4907
  setIsRefreshing(true);
3821
4908
  } else {
@@ -3832,10 +4919,10 @@ var ProfileView = ({
3832
4919
  setLoading(false);
3833
4920
  setIsRefreshing(false);
3834
4921
  }, [fetchData]);
3835
- useEffect4(() => {
4922
+ useEffect5(() => {
3836
4923
  load();
3837
4924
  }, [load, refreshCount, manualRefreshCount]);
3838
- useInput3((input, key) => {
4925
+ useInput4((input, key) => {
3839
4926
  if (input === "q" || key.escape) {
3840
4927
  exit();
3841
4928
  return;
@@ -3844,67 +4931,84 @@ var ProfileView = ({
3844
4931
  triggerManualRefresh();
3845
4932
  setManualRefreshCount((c) => c + 1);
3846
4933
  }
3847
- if (key.leftArrow || input === "1") {
3848
- setActiveTab(0);
4934
+ if (input === "1") setActiveTab(0);
4935
+ if (input === "2") setActiveTab(1);
4936
+ if (input === "3") setActiveTab(2);
4937
+ if (key.leftArrow) {
4938
+ setActiveTab((t) => t > 0 ? t - 1 : t);
3849
4939
  }
3850
- if (key.rightArrow || input === "2") {
3851
- setActiveTab(1);
4940
+ if (key.rightArrow) {
4941
+ setActiveTab(
4942
+ (t) => t < TAB_NAMES2.length - 1 ? t + 1 : t
4943
+ );
3852
4944
  }
3853
4945
  });
3854
4946
  if (error && !data) {
3855
- return /* @__PURE__ */ jsxs7(
3856
- Box7,
4947
+ return /* @__PURE__ */ jsxs15(
4948
+ Box15,
3857
4949
  {
3858
4950
  flexDirection: "column",
3859
4951
  paddingLeft: 1,
3860
4952
  paddingTop: 1,
3861
4953
  paddingBottom: 1,
3862
4954
  children: [
3863
- /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
4955
+ /* @__PURE__ */ jsxs15(Text15, { color: "red", children: [
3864
4956
  "Error: ",
3865
4957
  error
3866
4958
  ] }),
3867
- /* @__PURE__ */ jsx10(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: "Press q to exit" }) })
4959
+ /* @__PURE__ */ jsx16(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text15, { dimColor: true, children: "Press q to exit" }) })
3868
4960
  ]
3869
4961
  }
3870
4962
  );
3871
4963
  }
3872
4964
  if (loading && !data) {
3873
- return /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
3874
- /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
4965
+ return /* @__PURE__ */ jsx16(Box15, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs15(Text15, { children: [
4966
+ /* @__PURE__ */ jsx16(Spinner4, { type: "dots" }),
3875
4967
  " Loading profile\u2026"
3876
4968
  ] }) });
3877
4969
  }
3878
4970
  if (!data) return null;
3879
4971
  const hints = [
3880
- "\u2190 \u2192 switch tab",
4972
+ "1/2/3 or \u2190 \u2192 switch tab",
3881
4973
  autoRefresh ? `r refresh (${secondsUntilRefresh}s)` : "r refresh"
3882
4974
  ];
3883
4975
  hints.push("q quit");
3884
4976
  const footer = hints.join(" \xB7 ");
3885
4977
  const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
3886
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3887
- isRefreshing && /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3888
- /* @__PURE__ */ jsx10(Spinner3, { type: "dots" }),
4978
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
4979
+ isRefreshing && /* @__PURE__ */ jsx16(Box15, { paddingLeft: 1, children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
4980
+ /* @__PURE__ */ jsx16(Spinner4, { type: "dots" }),
3889
4981
  " Refreshing\u2026"
3890
4982
  ] }) }),
3891
- /* @__PURE__ */ jsxs7(Box7, { paddingLeft: 1, paddingTop: 1, gap: 2, children: [
3892
- TAB_NAMES.map((name, i) => /* @__PURE__ */ jsx10(Text7, { bold: activeTab === i, dimColor: activeTab !== i, children: activeTab === i ? `[${name}]` : name }, name)),
3893
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4983
+ /* @__PURE__ */ jsxs15(Box15, { paddingLeft: 1, paddingTop: 1, gap: 2, children: [
4984
+ TAB_NAMES2.map((name, i) => /* @__PURE__ */ jsx16(Text15, { bold: activeTab === i, dimColor: activeTab !== i, children: activeTab === i ? `[${name}]` : name }, name)),
4985
+ /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
3894
4986
  " ",
3895
4987
  identifier
3896
4988
  ] })
3897
4989
  ] }),
3898
- activeTab === 0 ? rankedPosts.length === 0 ? /* @__PURE__ */ jsx10(
3899
- Box7,
4990
+ activeTab === 0 ? data.postsError ? /* @__PURE__ */ jsx16(
4991
+ Box15,
4992
+ {
4993
+ flexDirection: "column",
4994
+ paddingLeft: 1,
4995
+ paddingTop: 1,
4996
+ paddingBottom: 1,
4997
+ children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
4998
+ "Could not load posts: ",
4999
+ data.postsError
5000
+ ] })
5001
+ }
5002
+ ) : rankedPosts.length === 0 ? /* @__PURE__ */ jsx16(
5003
+ Box15,
3900
5004
  {
3901
5005
  flexDirection: "column",
3902
5006
  paddingLeft: 1,
3903
5007
  paddingTop: 1,
3904
5008
  paddingBottom: 1,
3905
- children: /* @__PURE__ */ jsx10(Text7, { children: "No posts found for this profile." })
5009
+ children: /* @__PURE__ */ jsx16(Text15, { children: "No posts found for this profile." })
3906
5010
  }
3907
- ) : /* @__PURE__ */ jsx10(
5011
+ ) : /* @__PURE__ */ jsx16(
3908
5012
  Table,
3909
5013
  {
3910
5014
  columns: postColumns,
@@ -3912,16 +5016,28 @@ var ProfileView = ({
3912
5016
  title: "Posts",
3913
5017
  subtitle: `${rankedPosts.length} of ${data.postsCount}`
3914
5018
  }
3915
- ) : data.holdings.length === 0 ? /* @__PURE__ */ jsx10(
3916
- Box7,
5019
+ ) : activeTab === 1 ? data.holdingsError ? /* @__PURE__ */ jsx16(
5020
+ Box15,
5021
+ {
5022
+ flexDirection: "column",
5023
+ paddingLeft: 1,
5024
+ paddingTop: 1,
5025
+ paddingBottom: 1,
5026
+ children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
5027
+ "Could not load holdings: ",
5028
+ data.holdingsError
5029
+ ] })
5030
+ }
5031
+ ) : data.holdings.length === 0 ? /* @__PURE__ */ jsx16(
5032
+ Box15,
3917
5033
  {
3918
5034
  flexDirection: "column",
3919
5035
  paddingLeft: 1,
3920
5036
  paddingTop: 1,
3921
5037
  paddingBottom: 1,
3922
- children: /* @__PURE__ */ jsx10(Text7, { children: "No holdings found for this profile." })
5038
+ children: /* @__PURE__ */ jsx16(Text15, { children: "No holdings found for this profile." })
3923
5039
  }
3924
- ) : /* @__PURE__ */ jsx10(
5040
+ ) : /* @__PURE__ */ jsx16(
3925
5041
  Table,
3926
5042
  {
3927
5043
  columns: balanceColumns,
@@ -3929,25 +5045,98 @@ var ProfileView = ({
3929
5045
  title: "Holdings",
3930
5046
  subtitle: `${data.holdings.length} of ${data.holdingsCount}`
3931
5047
  }
5048
+ ) : data.tradesError ? /* @__PURE__ */ jsx16(
5049
+ Box15,
5050
+ {
5051
+ flexDirection: "column",
5052
+ paddingLeft: 1,
5053
+ paddingTop: 1,
5054
+ paddingBottom: 1,
5055
+ children: /* @__PURE__ */ jsxs15(Text15, { dimColor: true, children: [
5056
+ "Could not load trades: ",
5057
+ data.tradesError
5058
+ ] })
5059
+ }
5060
+ ) : data.trades.length === 0 ? /* @__PURE__ */ jsx16(
5061
+ Box15,
5062
+ {
5063
+ flexDirection: "column",
5064
+ paddingLeft: 1,
5065
+ paddingTop: 1,
5066
+ paddingBottom: 1,
5067
+ children: /* @__PURE__ */ jsx16(Text15, { children: "No trades found for this profile." })
5068
+ }
5069
+ ) : /* @__PURE__ */ jsx16(
5070
+ Table,
5071
+ {
5072
+ columns: tradeColumns,
5073
+ data: data.trades,
5074
+ title: "Trades",
5075
+ subtitle: `${data.trades.length} of ${data.tradesCount}`
5076
+ }
3932
5077
  ),
3933
- /* @__PURE__ */ jsx10(Box7, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx10(Text7, { dimColor: true, children: footer }) })
5078
+ /* @__PURE__ */ jsx16(Box15, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx16(Text15, { dimColor: true, children: footer }) })
3934
5079
  ] });
3935
5080
  };
3936
5081
 
3937
- // src/commands/profile.tsx
3938
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
3939
- var extractErrorMessage2 = (error) => {
3940
- if (typeof error === "object" && error !== null && "error" in error) {
3941
- return String(error.error);
3942
- }
3943
- return JSON.stringify(error);
5082
+ // src/components/ProfileHoldingsView.tsx
5083
+ import { Box as Box16, Text as Text16 } from "ink";
5084
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
5085
+ var emptyState7 = /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: [
5086
+ /* @__PURE__ */ jsx17(Text16, { children: "No holdings found for this profile." }),
5087
+ /* @__PURE__ */ jsxs16(Box16, { marginTop: 1, flexDirection: "column", children: [
5088
+ /* @__PURE__ */ jsx17(Text16, { dimColor: true, children: "Buy coins to see them here:" }),
5089
+ /* @__PURE__ */ jsxs16(Text16, { dimColor: true, children: [
5090
+ " zora buy ",
5091
+ "<address>",
5092
+ " --eth 0.001"
5093
+ ] })
5094
+ ] }),
5095
+ /* @__PURE__ */ jsx17(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx17(Text16, { dimColor: true, children: "Press q to exit" }) })
5096
+ ] });
5097
+ var ProfileHoldingsView = ({
5098
+ fetchPage,
5099
+ identifier,
5100
+ limit,
5101
+ autoRefresh,
5102
+ intervalSeconds
5103
+ }) => {
5104
+ return /* @__PURE__ */ jsx17(
5105
+ PaginatedTableView,
5106
+ {
5107
+ fetchPage,
5108
+ columns: balanceColumns,
5109
+ title: `Holdings \xB7 ${identifier}`,
5110
+ loadingText: "Loading holdings\u2026",
5111
+ emptyState: emptyState7,
5112
+ getAddress: (holding) => holding.coin?.address,
5113
+ limit,
5114
+ autoRefresh,
5115
+ intervalSeconds
5116
+ }
5117
+ );
3944
5118
  };
3945
- var resolveApiKey = () => {
5119
+
5120
+ // src/commands/profile.tsx
5121
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
5122
+ var resolveApiKey2 = () => {
3946
5123
  const apiKey = getApiKey();
3947
5124
  if (apiKey) {
3948
- setApiKey7(apiKey);
5125
+ setApiKey6(apiKey);
3949
5126
  }
3950
5127
  };
5128
+ var formatTradeJson2 = (trade, rank) => ({
5129
+ rank,
5130
+ side: trade.swapActivityType ?? "UNKNOWN",
5131
+ coinName: trade.coin?.name ?? null,
5132
+ coinSymbol: trade.coin?.symbol ?? null,
5133
+ coinType: trade.coin?.coinType ?? null,
5134
+ coinAddress: trade.coin?.address ?? null,
5135
+ coinAmount: trade.coinAmount,
5136
+ amountUsd: trade.currencyAmountWithPrice.amountUsd ?? null,
5137
+ transactionHash: trade.transactionHash,
5138
+ timestamp: trade.blockTimestamp
5139
+ });
3951
5140
  var formatPostJson = (post, rank) => ({
3952
5141
  rank,
3953
5142
  name: post.name,
@@ -3974,78 +5163,272 @@ var formatHoldingJson = (balance) => {
3974
5163
  marketCap: balance.coin?.marketCap ? Number(balance.coin.marketCap) : null
3975
5164
  };
3976
5165
  };
5166
+ var extractSectionError = (result) => {
5167
+ if (result.status === "rejected") {
5168
+ return result.reason instanceof Error ? result.reason.message : String(result.reason);
5169
+ }
5170
+ if (result.value.error) {
5171
+ return extractErrorMessage(result.value.error);
5172
+ }
5173
+ return void 0;
5174
+ };
3977
5175
  var fetchProfileData = async (identifier) => {
3978
- const [postsResult, holdingsResult] = await Promise.allSettled([
5176
+ const [postsResult, holdingsResult, tradesResult] = await Promise.allSettled([
3979
5177
  getProfileCoins({ identifier, count: 20 }),
3980
- getProfileBalances2({ identifier, count: 20, sortOption: "USD_VALUE" })
5178
+ getProfileBalances2({ identifier, count: 20, sortOption: "USD_VALUE" }),
5179
+ getWalletTradeActivity({ identifier, first: 20 })
3981
5180
  ]);
3982
- if (postsResult.status === "rejected") {
5181
+ const postsError = extractSectionError(postsResult);
5182
+ const holdingsError = extractSectionError(holdingsResult);
5183
+ const tradesError = extractSectionError(tradesResult);
5184
+ if (postsError && holdingsError && tradesError) {
3983
5185
  throw new Error(
3984
- postsResult.reason instanceof Error ? postsResult.reason.message : String(postsResult.reason)
5186
+ `All requests failed \u2014 posts: ${postsError}, holdings: ${holdingsError}, trades: ${tradesError}`
3985
5187
  );
3986
5188
  }
3987
- if (holdingsResult.status === "rejected") {
3988
- throw new Error(
3989
- holdingsResult.reason instanceof Error ? holdingsResult.reason.message : String(holdingsResult.reason)
5189
+ let posts = [];
5190
+ let postsCount = 0;
5191
+ if (!postsError && postsResult.status === "fulfilled") {
5192
+ const postEdges = postsResult.value.data?.profile?.createdCoins?.edges ?? [];
5193
+ posts = postEdges.map((e) => e.node);
5194
+ postsCount = postsResult.value.data?.profile?.createdCoins?.count ?? posts.length;
5195
+ }
5196
+ let holdings = [];
5197
+ let holdingsCount = 0;
5198
+ if (!holdingsError && holdingsResult.status === "fulfilled") {
5199
+ const holdingEdges = holdingsResult.value.data?.profile?.coinBalances?.edges ?? [];
5200
+ holdings = holdingEdges.map((e, i) => ({
5201
+ ...e.node,
5202
+ rank: i + 1
5203
+ }));
5204
+ holdingsCount = holdingsResult.value.data?.profile?.coinBalances?.count ?? holdings.length;
5205
+ }
5206
+ let trades = [];
5207
+ let tradesCount = 0;
5208
+ if (!tradesError && tradesResult.status === "fulfilled") {
5209
+ const tradeEdges = tradesResult.value.data?.walletAddressTradeActivity?.edges ?? [];
5210
+ trades = tradeEdges.map((e, i) => ({
5211
+ ...e.node,
5212
+ rank: i + 1
5213
+ }));
5214
+ tradesCount = tradesResult.value.data?.walletAddressTradeActivity?.count ?? trades.length;
5215
+ }
5216
+ return {
5217
+ posts,
5218
+ postsCount,
5219
+ postsError,
5220
+ holdings,
5221
+ holdingsCount,
5222
+ holdingsError,
5223
+ trades,
5224
+ tradesCount,
5225
+ tradesError
5226
+ };
5227
+ };
5228
+ var resolveIdentifier = (identifierArg, json) => {
5229
+ if (identifierArg) return identifierArg;
5230
+ const envKey = process.env.ZORA_PRIVATE_KEY;
5231
+ const key = envKey || getPrivateKey();
5232
+ if (!key) {
5233
+ return outputErrorAndExit(
5234
+ json,
5235
+ "No identifier provided and no wallet configured.",
5236
+ "Pass an address or handle, or run 'zora setup' first."
5237
+ );
5238
+ }
5239
+ try {
5240
+ return privateKeyToAccount3(normalizeKey(key)).address;
5241
+ } catch {
5242
+ return outputErrorAndExit(
5243
+ json,
5244
+ "Invalid wallet key. Run 'zora setup --force' to replace it."
5245
+ );
5246
+ }
5247
+ };
5248
+ var profileCommand = new Command7("profile").description("View profile activity (posts, holdings, and trades)").argument(
5249
+ "[identifier]",
5250
+ "Wallet address or profile handle (defaults to your wallet)"
5251
+ ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
5252
+ "--refresh <seconds>",
5253
+ "Auto-refresh interval in seconds, requires --live (min 5)",
5254
+ "30"
5255
+ ).action(async function(identifierArg) {
5256
+ const output = getOutputMode(this, "live");
5257
+ const json = output === "json";
5258
+ resolveApiKey2();
5259
+ const { live, intervalSeconds } = getLiveConfig(this, output);
5260
+ const identifier = resolveIdentifier(identifierArg, json);
5261
+ if (json) {
5262
+ const data = await fetchProfileData(identifier).catch(
5263
+ (err) => outputErrorAndExit(
5264
+ json,
5265
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
5266
+ )
5267
+ );
5268
+ outputData(json, {
5269
+ json: {
5270
+ posts: data.postsError ? { error: data.postsError } : data.posts.map((p, i) => formatPostJson(p, i + 1)),
5271
+ holdings: data.holdingsError ? { error: data.holdingsError } : data.holdings.map(formatHoldingJson),
5272
+ trades: data.tradesError ? { error: data.tradesError } : data.trades.map((t) => formatTradeJson2(t, t.rank))
5273
+ },
5274
+ render: () => {
5275
+ }
5276
+ });
5277
+ track("cli_profile", {
5278
+ identifier,
5279
+ output_format: "json",
5280
+ posts_count: data.postsCount,
5281
+ holdings_count: data.holdingsCount,
5282
+ trades_count: data.tradesCount
5283
+ });
5284
+ } else if (live) {
5285
+ const fetchData = () => fetchProfileData(identifier);
5286
+ await renderLive(
5287
+ /* @__PURE__ */ jsx18(
5288
+ ProfileView,
5289
+ {
5290
+ fetchData,
5291
+ identifier,
5292
+ autoRefresh: live,
5293
+ intervalSeconds
5294
+ }
5295
+ )
5296
+ );
5297
+ track("cli_profile", {
5298
+ identifier,
5299
+ output_format: "live",
5300
+ live,
5301
+ interval: intervalSeconds
5302
+ });
5303
+ } else {
5304
+ const data = await fetchProfileData(identifier).catch(
5305
+ (err) => outputErrorAndExit(
5306
+ json,
5307
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
5308
+ )
5309
+ );
5310
+ const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
5311
+ renderOnce(
5312
+ /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
5313
+ data.postsError ? /* @__PURE__ */ jsx18(Box17, { paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
5314
+ "Could not load posts: ",
5315
+ data.postsError
5316
+ ] }) }) : rankedPosts.length === 0 ? /* @__PURE__ */ jsx18(
5317
+ Box17,
5318
+ {
5319
+ flexDirection: "column",
5320
+ paddingLeft: 1,
5321
+ paddingTop: 1,
5322
+ paddingBottom: 1,
5323
+ children: /* @__PURE__ */ jsx18(Box17, { children: /* @__PURE__ */ jsx18(Text17, { children: "No posts found for this profile." }) })
5324
+ }
5325
+ ) : /* @__PURE__ */ jsx18(
5326
+ Table,
5327
+ {
5328
+ columns: postColumns,
5329
+ data: rankedPosts,
5330
+ title: "Posts",
5331
+ subtitle: `${rankedPosts.length} of ${data.postsCount}`
5332
+ }
5333
+ ),
5334
+ data.holdingsError ? /* @__PURE__ */ jsx18(Box17, { paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
5335
+ "Could not load holdings: ",
5336
+ data.holdingsError
5337
+ ] }) }) : data.holdings.length === 0 ? /* @__PURE__ */ jsx18(
5338
+ Box17,
5339
+ {
5340
+ flexDirection: "column",
5341
+ paddingLeft: 1,
5342
+ paddingTop: 1,
5343
+ paddingBottom: 1,
5344
+ children: /* @__PURE__ */ jsx18(Box17, { children: /* @__PURE__ */ jsx18(Text17, { children: "No holdings found for this profile." }) })
5345
+ }
5346
+ ) : /* @__PURE__ */ jsx18(
5347
+ Table,
5348
+ {
5349
+ columns: balanceColumns,
5350
+ data: data.holdings,
5351
+ title: "Holdings",
5352
+ subtitle: `${data.holdings.length} of ${data.holdingsCount}`
5353
+ }
5354
+ ),
5355
+ data.tradesError ? /* @__PURE__ */ jsx18(Box17, { paddingLeft: 1, paddingTop: 1, paddingBottom: 1, children: /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
5356
+ "Could not load trades: ",
5357
+ data.tradesError
5358
+ ] }) }) : data.trades.length === 0 ? /* @__PURE__ */ jsx18(
5359
+ Box17,
5360
+ {
5361
+ flexDirection: "column",
5362
+ paddingLeft: 1,
5363
+ paddingTop: 1,
5364
+ paddingBottom: 1,
5365
+ children: /* @__PURE__ */ jsx18(Box17, { children: /* @__PURE__ */ jsx18(Text17, { children: "No trades found for this profile." }) })
5366
+ }
5367
+ ) : /* @__PURE__ */ jsx18(
5368
+ Table,
5369
+ {
5370
+ columns: tradeColumns,
5371
+ data: data.trades,
5372
+ title: "Trades",
5373
+ subtitle: `${data.trades.length} of ${data.tradesCount}`
5374
+ }
5375
+ )
5376
+ ] })
3990
5377
  );
5378
+ track("cli_profile", {
5379
+ identifier,
5380
+ output_format: "static",
5381
+ posts_count: data.postsCount,
5382
+ holdings_count: data.holdingsCount,
5383
+ trades_count: data.tradesCount
5384
+ });
3991
5385
  }
3992
- if (postsResult.value.error) {
3993
- throw new Error(
3994
- `API error (posts): ${extractErrorMessage2(postsResult.value.error)}`
3995
- );
5386
+ });
5387
+ async function fetchPostsPage(identifier, count, after) {
5388
+ const response = await getProfileCoins({ identifier, count, after });
5389
+ if (response.error) {
5390
+ throw new Error(`API error: ${extractErrorMessage(response.error)}`);
3996
5391
  }
3997
- if (holdingsResult.value.error) {
3998
- throw new Error(
3999
- `API error (holdings): ${extractErrorMessage2(holdingsResult.value.error)}`
4000
- );
5392
+ const edges = response.data?.profile?.createdCoins?.edges ?? [];
5393
+ const items = edges.map((e) => e.node);
5394
+ const total = response.data?.profile?.createdCoins?.count ?? items.length;
5395
+ const pageInfo = response.data?.profile?.createdCoins?.pageInfo;
5396
+ return { items, count: total, pageInfo };
5397
+ }
5398
+ async function fetchHoldingsPage(identifier, count, sortOption, after) {
5399
+ const response = await getProfileBalances2({
5400
+ identifier,
5401
+ count,
5402
+ sortOption,
5403
+ after
5404
+ });
5405
+ if (response.error) {
5406
+ throw new Error(`API error: ${extractErrorMessage(response.error)}`);
4001
5407
  }
4002
- const postEdges = postsResult.value.data?.profile?.createdCoins?.edges ?? [];
4003
- const posts = postEdges.map((e) => e.node);
4004
- const postsCount = postsResult.value.data?.profile?.createdCoins?.count ?? posts.length;
4005
- const holdingEdges = holdingsResult.value.data?.profile?.coinBalances?.edges ?? [];
4006
- const holdings = holdingEdges.map(
4007
- (e, i) => ({
4008
- ...e.node,
4009
- rank: i + 1
4010
- })
4011
- );
4012
- const holdingsCount = holdingsResult.value.data?.profile?.coinBalances?.count ?? holdings.length;
4013
- return { posts, postsCount, holdings, holdingsCount };
4014
- };
4015
- var profileCommand = new Command8("profile").description("View profile activity (posts and holdings)").argument(
5408
+ const edges = response.data?.profile?.coinBalances?.edges ?? [];
5409
+ const items = edges.map((e) => e.node);
5410
+ const total = response.data?.profile?.coinBalances?.count ?? items.length;
5411
+ const pageInfo = response.data?.profile?.coinBalances?.pageInfo;
5412
+ return { items, count: total, pageInfo };
5413
+ }
5414
+ profileCommand.command("posts").description("View profile posts (created coins) with pagination").argument(
4016
5415
  "[identifier]",
4017
5416
  "Wallet address or profile handle (defaults to your wallet)"
4018
- ).option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
5417
+ ).option("--limit <n>", "Number of results per page (max 20)", "10").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
4019
5418
  "--refresh <seconds>",
4020
5419
  "Auto-refresh interval in seconds, requires --live (min 5)",
4021
5420
  "30"
4022
- ).action(async function(identifierArg) {
5421
+ ).option("--after <cursor>", "Pagination cursor from a previous result").action(async function(identifierArg) {
4023
5422
  const output = getOutputMode(this, "live");
4024
5423
  const json = output === "json";
4025
- resolveApiKey();
5424
+ resolveApiKey2();
5425
+ const opts = this.opts();
5426
+ const after = opts.after;
5427
+ const limit = Math.min(20, Math.max(1, parseInt(opts.limit, 10) || 10));
4026
5428
  const { live, intervalSeconds } = getLiveConfig(this, output);
4027
- let identifier = identifierArg;
4028
- if (!identifier) {
4029
- const envKey = process.env.ZORA_PRIVATE_KEY;
4030
- const key = envKey || getPrivateKey();
4031
- if (!key) {
4032
- outputErrorAndExit(
4033
- json,
4034
- "No identifier provided and no wallet configured.",
4035
- "Pass an address or handle, or run 'zora setup' first."
4036
- );
4037
- }
4038
- try {
4039
- identifier = privateKeyToAccount3(normalizeKey(key)).address;
4040
- } catch {
4041
- outputErrorAndExit(
4042
- json,
4043
- "Invalid wallet key. Run 'zora setup --force' to replace it."
4044
- );
4045
- }
4046
- }
5429
+ const identifier = resolveIdentifier(identifierArg, json);
4047
5430
  if (json) {
4048
- const data = await fetchProfileData(identifier).catch(
5431
+ const result = await fetchPostsPage(identifier, limit, after).catch(
4049
5432
  (err) => outputErrorAndExit(
4050
5433
  json,
4051
5434
  `Request failed: ${err instanceof Error ? err.message : String(err)}`
@@ -4053,96 +5436,358 @@ var profileCommand = new Command8("profile").description("View profile activity
4053
5436
  );
4054
5437
  outputData(json, {
4055
5438
  json: {
4056
- posts: data.posts.map((p, i) => formatPostJson(p, i + 1)),
4057
- holdings: data.holdings.map(formatHoldingJson)
5439
+ posts: result.items.map((p, i) => formatPostJson(p, i + 1)),
5440
+ pageInfo: result.pageInfo ?? null
4058
5441
  },
4059
5442
  render: () => {
4060
5443
  }
4061
5444
  });
4062
- track("cli_profile", {
5445
+ track("cli_profile_posts", {
4063
5446
  identifier,
4064
5447
  output_format: "json",
4065
- posts_count: data.postsCount,
4066
- holdings_count: data.holdingsCount
5448
+ count: result.count
4067
5449
  });
4068
5450
  } else if (live) {
4069
- const fetchData = () => fetchProfileData(identifier);
5451
+ const fetchPage = (cursor) => fetchPostsPage(identifier, limit, cursor);
4070
5452
  await renderLive(
4071
- /* @__PURE__ */ jsx11(
4072
- ProfileView,
5453
+ /* @__PURE__ */ jsx18(
5454
+ ProfilePostsView,
4073
5455
  {
4074
- fetchData,
5456
+ fetchPage,
4075
5457
  identifier,
5458
+ limit,
4076
5459
  autoRefresh: live,
4077
5460
  intervalSeconds
4078
5461
  }
4079
5462
  )
4080
5463
  );
4081
- track("cli_profile", {
5464
+ track("cli_profile_posts", {
4082
5465
  identifier,
4083
5466
  output_format: "live",
4084
5467
  live,
4085
5468
  interval: intervalSeconds
4086
5469
  });
4087
5470
  } else {
4088
- const data = await fetchProfileData(identifier).catch(
5471
+ const result = await fetchPostsPage(identifier, limit, after).catch(
4089
5472
  (err) => outputErrorAndExit(
4090
5473
  json,
4091
5474
  `Request failed: ${err instanceof Error ? err.message : String(err)}`
4092
5475
  )
4093
5476
  );
4094
- const rankedPosts = data.posts.map((p, i) => ({ ...p, rank: i + 1 }));
4095
- renderOnce(
4096
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
4097
- rankedPosts.length === 0 ? /* @__PURE__ */ jsx11(
4098
- Box8,
5477
+ const rankedPosts = result.items.map((p, i) => ({
5478
+ ...p,
5479
+ rank: i + 1
5480
+ }));
5481
+ if (rankedPosts.length === 0) {
5482
+ renderOnce(
5483
+ /* @__PURE__ */ jsx18(
5484
+ Box17,
4099
5485
  {
4100
5486
  flexDirection: "column",
4101
5487
  paddingLeft: 1,
4102
5488
  paddingTop: 1,
4103
5489
  paddingBottom: 1,
4104
- children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No posts found for this profile." }) })
5490
+ children: /* @__PURE__ */ jsx18(Text17, { children: "No posts found for this profile." })
4105
5491
  }
4106
- ) : /* @__PURE__ */ jsx11(
5492
+ )
5493
+ );
5494
+ } else {
5495
+ const footer = result.pageInfo?.hasNextPage && result.pageInfo.endCursor ? `Next page: zora profile posts ${identifier} --limit ${limit} --after ${result.pageInfo.endCursor}` : void 0;
5496
+ renderOnce(
5497
+ /* @__PURE__ */ jsx18(
4107
5498
  Table,
4108
5499
  {
4109
5500
  columns: postColumns,
4110
5501
  data: rankedPosts,
4111
5502
  title: "Posts",
4112
- subtitle: `${rankedPosts.length} of ${data.postsCount}`
5503
+ subtitle: `${rankedPosts.length} of ${result.count}`,
5504
+ footer
4113
5505
  }
4114
- ),
4115
- data.holdings.length === 0 ? /* @__PURE__ */ jsx11(
4116
- Box8,
5506
+ )
5507
+ );
5508
+ }
5509
+ track("cli_profile_posts", {
5510
+ identifier,
5511
+ output_format: "static",
5512
+ count: result.count
5513
+ });
5514
+ }
5515
+ });
5516
+ var SORT_MAP2 = {
5517
+ "usd-value": "USD_VALUE",
5518
+ balance: "BALANCE",
5519
+ "market-cap": "MARKET_CAP",
5520
+ "price-change": "PRICE_CHANGE"
5521
+ };
5522
+ var SORT_OPTIONS3 = Object.keys(SORT_MAP2).join(", ");
5523
+ profileCommand.command("holdings").description("View profile holdings (coin balances) with pagination").argument(
5524
+ "[identifier]",
5525
+ "Wallet address or profile handle (defaults to your wallet)"
5526
+ ).option("--sort <sort>", `Sort by: ${SORT_OPTIONS3}`, "usd-value").option("--limit <n>", "Number of results per page (max 20)", "10").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
5527
+ "--refresh <seconds>",
5528
+ "Auto-refresh interval in seconds, requires --live (min 5)",
5529
+ "30"
5530
+ ).option("--after <cursor>", "Pagination cursor from a previous result").action(async function(identifierArg) {
5531
+ const output = getOutputMode(this, "live");
5532
+ const json = output === "json";
5533
+ resolveApiKey2();
5534
+ const opts = this.opts();
5535
+ const after = opts.after;
5536
+ const sort = opts.sort;
5537
+ const sortOption = SORT_MAP2[sort];
5538
+ if (!sortOption) {
5539
+ outputErrorAndExit(
5540
+ json,
5541
+ `Invalid --sort value: ${sort}.`,
5542
+ `Supported: ${SORT_OPTIONS3}`
5543
+ );
5544
+ }
5545
+ const limit = Math.min(20, Math.max(1, parseInt(opts.limit, 10) || 10));
5546
+ const { live, intervalSeconds } = getLiveConfig(this, output);
5547
+ const identifier = resolveIdentifier(identifierArg, json);
5548
+ if (json) {
5549
+ const result = await fetchHoldingsPage(
5550
+ identifier,
5551
+ limit,
5552
+ sortOption,
5553
+ after
5554
+ ).catch(
5555
+ (err) => outputErrorAndExit(
5556
+ json,
5557
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
5558
+ )
5559
+ );
5560
+ const rankedHoldings = result.items.map((h, i) => ({
5561
+ ...h,
5562
+ rank: i + 1
5563
+ }));
5564
+ outputData(json, {
5565
+ json: {
5566
+ holdings: rankedHoldings.map(formatHoldingJson),
5567
+ pageInfo: result.pageInfo ?? null
5568
+ },
5569
+ render: () => {
5570
+ }
5571
+ });
5572
+ track("cli_profile_holdings", {
5573
+ identifier,
5574
+ output_format: "json",
5575
+ sort,
5576
+ count: result.count
5577
+ });
5578
+ } else if (live) {
5579
+ const fetchPage = (cursor) => fetchHoldingsPage(identifier, limit, sortOption, cursor);
5580
+ await renderLive(
5581
+ /* @__PURE__ */ jsx18(
5582
+ ProfileHoldingsView,
5583
+ {
5584
+ fetchPage,
5585
+ identifier,
5586
+ limit,
5587
+ autoRefresh: live,
5588
+ intervalSeconds
5589
+ }
5590
+ )
5591
+ );
5592
+ track("cli_profile_holdings", {
5593
+ identifier,
5594
+ output_format: "live",
5595
+ live,
5596
+ sort,
5597
+ interval: intervalSeconds
5598
+ });
5599
+ } else {
5600
+ const result = await fetchHoldingsPage(
5601
+ identifier,
5602
+ limit,
5603
+ sortOption,
5604
+ after
5605
+ ).catch(
5606
+ (err) => outputErrorAndExit(
5607
+ json,
5608
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
5609
+ )
5610
+ );
5611
+ const rankedHoldings = result.items.map((h, i) => ({
5612
+ ...h,
5613
+ rank: i + 1
5614
+ }));
5615
+ if (rankedHoldings.length === 0) {
5616
+ renderOnce(
5617
+ /* @__PURE__ */ jsxs17(
5618
+ Box17,
4117
5619
  {
4118
5620
  flexDirection: "column",
4119
5621
  paddingLeft: 1,
4120
5622
  paddingTop: 1,
4121
5623
  paddingBottom: 1,
4122
- children: /* @__PURE__ */ jsx11(Box8, { children: /* @__PURE__ */ jsx11(Text8, { children: "No holdings found for this profile." }) })
5624
+ children: [
5625
+ /* @__PURE__ */ jsx18(Text17, { children: "No holdings found for this profile." }),
5626
+ /* @__PURE__ */ jsxs17(Box17, { marginTop: 1, flexDirection: "column", children: [
5627
+ /* @__PURE__ */ jsx18(Text17, { dimColor: true, children: "Buy coins to see them here:" }),
5628
+ /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
5629
+ " zora buy ",
5630
+ "<address>",
5631
+ " --eth 0.001"
5632
+ ] })
5633
+ ] })
5634
+ ]
4123
5635
  }
4124
- ) : /* @__PURE__ */ jsx11(
5636
+ )
5637
+ );
5638
+ } else {
5639
+ const footer = result.pageInfo?.hasNextPage && result.pageInfo.endCursor ? `Next page: zora profile holdings ${identifier} --sort ${sort} --limit ${limit} --after ${result.pageInfo.endCursor}` : void 0;
5640
+ renderOnce(
5641
+ /* @__PURE__ */ jsx18(
4125
5642
  Table,
4126
5643
  {
4127
5644
  columns: balanceColumns,
4128
- data: data.holdings,
4129
- title: "Holdings",
4130
- subtitle: `${data.holdings.length} of ${data.holdingsCount}`
5645
+ data: rankedHoldings,
5646
+ title: `Holdings \xB7 sorted by ${sort}`,
5647
+ subtitle: `${rankedHoldings.length} of ${result.count}`,
5648
+ footer
4131
5649
  }
4132
5650
  )
4133
- ] })
5651
+ );
5652
+ }
5653
+ track("cli_profile_holdings", {
5654
+ identifier,
5655
+ output_format: "static",
5656
+ sort,
5657
+ count: result.count
5658
+ });
5659
+ }
5660
+ });
5661
+ async function fetchTradesPage2(identifier, count, after) {
5662
+ const response = await getWalletTradeActivity({
5663
+ identifier,
5664
+ first: count,
5665
+ after
5666
+ });
5667
+ if (response.error) {
5668
+ throw new Error(`API error: ${extractErrorMessage(response.error)}`);
5669
+ }
5670
+ const edges = response.data?.walletAddressTradeActivity?.edges ?? [];
5671
+ const items = edges.map((e) => e.node);
5672
+ const total = response.data?.walletAddressTradeActivity?.count ?? items.length;
5673
+ const pageInfo = response.data?.walletAddressTradeActivity?.pageInfo;
5674
+ return { items, count: total, pageInfo };
5675
+ }
5676
+ profileCommand.command("trades").description("View profile trade activity (buys and sells) with pagination").argument(
5677
+ "[identifier]",
5678
+ "Wallet address or profile handle (defaults to your wallet)"
5679
+ ).option("--limit <n>", "Number of results per page (max 20)", "10").option("--live", "Interactive live-updating display (default)").option("--static", "Static snapshot").option(
5680
+ "--refresh <seconds>",
5681
+ "Auto-refresh interval in seconds, requires --live (min 5)",
5682
+ "30"
5683
+ ).option("--after <cursor>", "Pagination cursor from a previous result").action(async function(identifierArg) {
5684
+ const output = getOutputMode(this, "live");
5685
+ const json = output === "json";
5686
+ resolveApiKey2();
5687
+ const opts = this.opts();
5688
+ const after = opts.after;
5689
+ const limit = Math.min(20, Math.max(1, parseInt(opts.limit, 10) || 10));
5690
+ const { live, intervalSeconds } = getLiveConfig(this, output);
5691
+ const identifier = resolveIdentifier(identifierArg, json);
5692
+ if (json) {
5693
+ const result = await fetchTradesPage2(identifier, limit, after).catch(
5694
+ (err) => outputErrorAndExit(
5695
+ json,
5696
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
5697
+ )
4134
5698
  );
4135
- track("cli_profile", {
5699
+ outputData(json, {
5700
+ json: {
5701
+ trades: result.items.map((t, i) => formatTradeJson2(t, i + 1)),
5702
+ pageInfo: result.pageInfo ?? null
5703
+ },
5704
+ render: () => {
5705
+ }
5706
+ });
5707
+ track("cli_profile_trades", {
5708
+ identifier,
5709
+ output_format: "json",
5710
+ count: result.count
5711
+ });
5712
+ } else if (live) {
5713
+ const fetchPage = (cursor) => fetchTradesPage2(identifier, limit, cursor);
5714
+ await renderLive(
5715
+ /* @__PURE__ */ jsx18(
5716
+ ProfileTradesView,
5717
+ {
5718
+ fetchPage,
5719
+ identifier,
5720
+ limit,
5721
+ autoRefresh: live,
5722
+ intervalSeconds
5723
+ }
5724
+ )
5725
+ );
5726
+ track("cli_profile_trades", {
5727
+ identifier,
5728
+ output_format: "live",
5729
+ live,
5730
+ interval: intervalSeconds
5731
+ });
5732
+ } else {
5733
+ const result = await fetchTradesPage2(identifier, limit, after).catch(
5734
+ (err) => outputErrorAndExit(
5735
+ json,
5736
+ `Request failed: ${err instanceof Error ? err.message : String(err)}`
5737
+ )
5738
+ );
5739
+ const rankedTrades = result.items.map((t, i) => ({
5740
+ ...t,
5741
+ rank: i + 1
5742
+ }));
5743
+ if (rankedTrades.length === 0) {
5744
+ renderOnce(
5745
+ /* @__PURE__ */ jsxs17(
5746
+ Box17,
5747
+ {
5748
+ flexDirection: "column",
5749
+ paddingLeft: 1,
5750
+ paddingTop: 1,
5751
+ paddingBottom: 1,
5752
+ children: [
5753
+ /* @__PURE__ */ jsx18(Text17, { children: "No trades found for this profile." }),
5754
+ /* @__PURE__ */ jsxs17(Box17, { marginTop: 1, flexDirection: "column", children: [
5755
+ /* @__PURE__ */ jsx18(Text17, { dimColor: true, children: "Buy coins to see trades here:" }),
5756
+ /* @__PURE__ */ jsxs17(Text17, { dimColor: true, children: [
5757
+ " zora buy ",
5758
+ "<address>",
5759
+ " --eth 0.001"
5760
+ ] })
5761
+ ] })
5762
+ ]
5763
+ }
5764
+ )
5765
+ );
5766
+ } else {
5767
+ const footer = result.pageInfo?.hasNextPage && result.pageInfo.endCursor ? `Next page: zora profile trades ${identifier} --limit ${limit} --after ${result.pageInfo.endCursor}` : void 0;
5768
+ renderOnce(
5769
+ /* @__PURE__ */ jsx18(
5770
+ Table,
5771
+ {
5772
+ columns: tradeColumns,
5773
+ data: rankedTrades,
5774
+ title: "Trades",
5775
+ subtitle: `${rankedTrades.length} of ${result.count}`,
5776
+ footer
5777
+ }
5778
+ )
5779
+ );
5780
+ }
5781
+ track("cli_profile_trades", {
4136
5782
  identifier,
4137
5783
  output_format: "static",
4138
- posts_count: data.postsCount,
4139
- holdings_count: data.holdingsCount
5784
+ count: result.count
4140
5785
  });
4141
5786
  }
4142
5787
  });
4143
5788
 
4144
5789
  // src/commands/send.ts
4145
- import { Command as Command9 } from "commander";
5790
+ import { Command as Command8 } from "commander";
4146
5791
  import confirm4 from "@inquirer/confirm";
4147
5792
  import {
4148
5793
  erc20Abi as erc20Abi4,
@@ -4150,7 +5795,7 @@ import {
4150
5795
  isAddress as isAddress3,
4151
5796
  parseUnits as parseUnits3
4152
5797
  } from "viem";
4153
- import { setApiKey as setApiKey8 } from "@zoralabs/coins-sdk";
5798
+ import { setApiKey as setApiKey7 } from "@zoralabs/coins-sdk";
4154
5799
  var SEND_AMOUNT_CHECKS = {
4155
5800
  amount: (opts) => opts.amount !== void 0,
4156
5801
  percent: (opts) => opts.percent !== void 0,
@@ -4198,7 +5843,7 @@ function printSendResult(json, info) {
4198
5843
  console.log(` Tx ${info.txHash}
4199
5844
  `);
4200
5845
  }
4201
- var sendCommand = new Command9("send").description("Send coins or ETH to an address").argument(
5846
+ var sendCommand = new Command8("send").description("Send coins or ETH to an address").argument(
4202
5847
  "[typeOrId]",
4203
5848
  "Token (eth, usdc, zora), type prefix (creator-coin, trend), or coin address/name"
4204
5849
  ).argument("[identifier]", "Coin name (when type prefix is given)").option("--to <address>", "Recipient address (0x...)").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(firstArg, secondArg, opts) {
@@ -4313,7 +5958,7 @@ var sendCommand = new Command9("send").description("Send coins or ETH to an addr
4313
5958
  const ok = await confirm4({ message: "Confirm?", default: false });
4314
5959
  if (!ok) {
4315
5960
  console.error("Aborted.");
4316
- process.exit(0);
5961
+ safeExit(SUCCESS);
4317
5962
  }
4318
5963
  }
4319
5964
  let txHash;
@@ -4370,7 +6015,7 @@ var sendCommand = new Command9("send").description("Send coins or ETH to an addr
4370
6015
  } else {
4371
6016
  const apiKey = getApiKey();
4372
6017
  if (apiKey) {
4373
- setApiKey8(apiKey);
6018
+ setApiKey7(apiKey);
4374
6019
  }
4375
6020
  let parsed;
4376
6021
  try {
@@ -4537,7 +6182,7 @@ var sendCommand = new Command9("send").description("Send coins or ETH to an addr
4537
6182
  const ok = await confirm4({ message: "Confirm?", default: false });
4538
6183
  if (!ok) {
4539
6184
  console.error("Aborted.");
4540
- process.exit(0);
6185
+ safeExit(SUCCESS);
4541
6186
  }
4542
6187
  }
4543
6188
  let txHash;
@@ -4592,8 +6237,8 @@ var sendCommand = new Command9("send").description("Send coins or ETH to an addr
4592
6237
  });
4593
6238
 
4594
6239
  // src/commands/setup.tsx
4595
- import { Command as Command10 } from "commander";
4596
- import { Text as Text9, Box as Box9 } from "ink";
6240
+ import { Command as Command9 } from "commander";
6241
+ import { Text as Text18, Box as Box18 } from "ink";
4597
6242
 
4598
6243
  // src/lib/strings.ts
4599
6244
  var DEPOSIT_SOURCES = "Deposit 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";
@@ -4756,7 +6401,7 @@ function warningBox(text) {
4756
6401
  }
4757
6402
 
4758
6403
  // src/commands/setup.tsx
4759
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
6404
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
4760
6405
  function stepLine(step, total, title) {
4761
6406
  const cols = (process.stdout.columns || 80) - 4;
4762
6407
  if (!useAnsi()) {
@@ -4773,7 +6418,7 @@ ${BOLD}${DIM}[${step}/${total}]${RESET} ${BOLD}${title}${RESET}`
4773
6418
  console.log(`${DIM}${"\u2500".repeat(Math.max(cols, 20))}${RESET}
4774
6419
  `);
4775
6420
  }
4776
- var setupCommand = new Command10("setup").description("Guided first-time setup").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) {
6421
+ var setupCommand = new Command9("setup").description("Guided first-time setup").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) {
4777
6422
  const json = getJson(this);
4778
6423
  const nonInteractive = getYes(this);
4779
6424
  if (!json) stepLine(1, 3, "Set up wallet");
@@ -4841,8 +6486,8 @@ var setupCommand = new Command10("setup").description("Guided first-time setup")
4841
6486
  if (!json) stepLine(3, 3, "Deposit");
4842
6487
  if (!json) {
4843
6488
  renderOnce(
4844
- /* @__PURE__ */ jsxs9(
4845
- Box9,
6489
+ /* @__PURE__ */ jsxs18(
6490
+ Box18,
4846
6491
  {
4847
6492
  flexDirection: "column",
4848
6493
  borderStyle: "single",
@@ -4850,14 +6495,14 @@ var setupCommand = new Command10("setup").description("Guided first-time setup")
4850
6495
  paddingX: 1,
4851
6496
  paddingY: 1,
4852
6497
  children: [
4853
- /* @__PURE__ */ jsxs9(Text9, { children: [
6498
+ /* @__PURE__ */ jsxs18(Text18, { children: [
4854
6499
  "Your address: ",
4855
- /* @__PURE__ */ jsx12(Text9, { bold: true, children: walletResult.address })
6500
+ /* @__PURE__ */ jsx19(Text18, { bold: true, children: walletResult.address })
4856
6501
  ] }),
4857
- /* @__PURE__ */ jsxs9(Text9, { children: [
6502
+ /* @__PURE__ */ jsxs18(Text18, { children: [
4858
6503
  "Deposit",
4859
6504
  " ",
4860
- /* @__PURE__ */ jsx12(Text9, { bold: true, color: "blue", children: "ETH or USDC on Base" }),
6505
+ /* @__PURE__ */ jsx19(Text18, { bold: true, color: "blue", children: "ETH or USDC on Base" }),
4861
6506
  " ",
4862
6507
  "to start trading."
4863
6508
  ] })
@@ -4910,7 +6555,7 @@ async function promptAndSaveApiKey(json, nonInteractive = false) {
4910
6555
  }
4911
6556
 
4912
6557
  // src/commands/wallet.ts
4913
- import { Command as Command11 } from "commander";
6558
+ import { Command as Command10 } from "commander";
4914
6559
  import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
4915
6560
  var resolvePrivateKey = () => {
4916
6561
  const envKey = process.env.ZORA_PRIVATE_KEY;
@@ -4923,7 +6568,7 @@ var resolvePrivateKey = () => {
4923
6568
  }
4924
6569
  return void 0;
4925
6570
  };
4926
- var walletCommand = new Command11("wallet").description("Manage your Zora wallet").action(function() {
6571
+ var walletCommand = new Command10("wallet").description("Manage your Zora wallet").action(function() {
4927
6572
  this.outputHelp();
4928
6573
  });
4929
6574
  walletCommand.command("info").description("Show wallet address and storage location").action(function() {
@@ -4973,7 +6618,7 @@ walletCommand.command("export").description("Print the raw private key to stdout
4973
6618
  );
4974
6619
  if (!ok) {
4975
6620
  console.error("Aborted.");
4976
- process.exit(0);
6621
+ safeExit(SUCCESS);
4977
6622
  }
4978
6623
  }
4979
6624
  console.log(resolved.key);
@@ -5031,19 +6676,19 @@ walletCommand.command("configure").description("Create or import a wallet").opti
5031
6676
  });
5032
6677
 
5033
6678
  // src/components/StyledHelp.tsx
5034
- import { Text as Text11, Box as Box11 } from "ink";
6679
+ import { Text as Text20, Box as Box20 } from "ink";
5035
6680
 
5036
6681
  // src/components/KeyValueTable.tsx
5037
- import { Text as Text10, Box as Box10 } from "ink";
5038
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
6682
+ import { Text as Text19, Box as Box19 } from "ink";
6683
+ import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
5039
6684
  function KeyValueTable({
5040
6685
  rows,
5041
6686
  labelWidth
5042
6687
  }) {
5043
6688
  const pad = labelWidth ?? Math.max(0, ...rows.map((r) => r.label.length)) + 2;
5044
- return /* @__PURE__ */ jsx13(Box10, { flexDirection: "column", children: rows.map((row, i) => /* @__PURE__ */ jsxs10(Text10, { children: [
5045
- /* @__PURE__ */ jsx13(Text10, { bold: true, children: row.label.padEnd(pad) }),
5046
- /* @__PURE__ */ jsx13(Text10, { dimColor: true, children: row.value })
6689
+ return /* @__PURE__ */ jsx20(Box19, { flexDirection: "column", children: rows.map((row, i) => /* @__PURE__ */ jsxs19(Text19, { children: [
6690
+ /* @__PURE__ */ jsx20(Text19, { bold: true, children: row.label.padEnd(pad) }),
6691
+ /* @__PURE__ */ jsx20(Text19, { dimColor: true, children: row.value })
5047
6692
  ] }, i)) });
5048
6693
  }
5049
6694
 
@@ -5084,17 +6729,17 @@ function parseHelpSections(text) {
5084
6729
  }
5085
6730
 
5086
6731
  // src/components/StyledHelp.tsx
5087
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
6732
+ import { jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
5088
6733
  function StyledHelp({
5089
6734
  sections,
5090
6735
  header
5091
6736
  }) {
5092
6737
  const descriptionColumnOffset = getDescriptionColumnOffset(sections);
5093
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", gap: 1, children: [
6738
+ return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", gap: 1, children: [
5094
6739
  header,
5095
- /* @__PURE__ */ jsxs11(Box11, { paddingX: 1, children: [
5096
- /* @__PURE__ */ jsx14(Text11, { color: "yellow", children: "\u26A0 Beta:" }),
5097
- /* @__PURE__ */ jsx14(Text11, { children: " This CLI is in beta and should be used with caution." })
6740
+ /* @__PURE__ */ jsxs20(Box20, { paddingX: 1, children: [
6741
+ /* @__PURE__ */ jsx21(Text20, { color: "yellow", children: "\u26A0 Beta:" }),
6742
+ /* @__PURE__ */ jsx21(Text20, { children: " This CLI is in beta and should be used with caution." })
5098
6743
  ] }),
5099
6744
  sections.map((section, i) => {
5100
6745
  const hasTwoColumns = TWO_COLUMN_REGEX.test(section.content);
@@ -5107,8 +6752,8 @@ function StyledHelp({
5107
6752
  };
5108
6753
  return { label: "", value: line.trimStart() };
5109
6754
  }) : null;
5110
- return /* @__PURE__ */ jsxs11(
5111
- Box11,
6755
+ return /* @__PURE__ */ jsxs20(
6756
+ Box20,
5112
6757
  {
5113
6758
  flexDirection: "column",
5114
6759
  borderStyle: "single",
@@ -5116,8 +6761,8 @@ function StyledHelp({
5116
6761
  paddingX: 1,
5117
6762
  paddingY: 1,
5118
6763
  children: [
5119
- /* @__PURE__ */ jsx14(Text11, { bold: true, children: section.title }),
5120
- rows ? /* @__PURE__ */ jsx14(KeyValueTable, { rows, labelWidth: descriptionColumnOffset }) : /* @__PURE__ */ jsx14(Text11, { children: section.content })
6764
+ /* @__PURE__ */ jsx21(Text20, { bold: true, children: section.title }),
6765
+ rows ? /* @__PURE__ */ jsx21(KeyValueTable, { rows, labelWidth: descriptionColumnOffset }) : /* @__PURE__ */ jsx21(Text20, { children: section.content })
5121
6766
  ]
5122
6767
  },
5123
6768
  i
@@ -5127,10 +6772,10 @@ function StyledHelp({
5127
6772
  }
5128
6773
 
5129
6774
  // src/components/StyledHelpHeader.tsx
5130
- import { Text as Text13, Box as Box13 } from "ink";
6775
+ import { Text as Text22, Box as Box22 } from "ink";
5131
6776
 
5132
6777
  // src/components/Zorb.tsx
5133
- import { Text as Text12, Box as Box12 } from "ink";
6778
+ import { Text as Text21, Box as Box21 } from "ink";
5134
6779
 
5135
6780
  // src/lib/zorb-pixels.ts
5136
6781
  function supportsTruecolor() {
@@ -5275,7 +6920,7 @@ function generateZorbPixels(size) {
5275
6920
  }
5276
6921
 
5277
6922
  // src/components/Zorb.tsx
5278
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
6923
+ import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
5279
6924
  var LOWER_HALF_BLOCK = "\u2584";
5280
6925
  var UPPER_HALF_BLOCK = "\u2580";
5281
6926
  function rgbString([r, g, b]) {
@@ -5298,19 +6943,19 @@ function Zorb({ size = 20 }) {
5298
6943
  const topIsBlack = isBlack(top);
5299
6944
  const bottomIsBlack = isBlack(bottom);
5300
6945
  if (topIsBlack && bottomIsBlack) {
5301
- cells.push(/* @__PURE__ */ jsx15(Text12, { children: " " }, x));
6946
+ cells.push(/* @__PURE__ */ jsx22(Text21, { children: " " }, x));
5302
6947
  } else if (topIsBlack) {
5303
6948
  cells.push(
5304
- /* @__PURE__ */ jsx15(Text12, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
6949
+ /* @__PURE__ */ jsx22(Text21, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
5305
6950
  );
5306
6951
  } else if (bottomIsBlack) {
5307
6952
  cells.push(
5308
- /* @__PURE__ */ jsx15(Text12, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
6953
+ /* @__PURE__ */ jsx22(Text21, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
5309
6954
  );
5310
6955
  } else {
5311
6956
  cells.push(
5312
- /* @__PURE__ */ jsx15(
5313
- Text12,
6957
+ /* @__PURE__ */ jsx22(
6958
+ Text21,
5314
6959
  {
5315
6960
  backgroundColor: rgbString(top),
5316
6961
  color: rgbString(bottom),
@@ -5321,23 +6966,23 @@ function Zorb({ size = 20 }) {
5321
6966
  );
5322
6967
  }
5323
6968
  }
5324
- rows.push(/* @__PURE__ */ jsx15(Text12, { children: cells }, y));
6969
+ rows.push(/* @__PURE__ */ jsx22(Text21, { children: cells }, y));
5325
6970
  }
5326
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
5327
- /* @__PURE__ */ jsx15(Text12, { children: " " }),
6971
+ return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
6972
+ /* @__PURE__ */ jsx22(Text21, { children: " " }),
5328
6973
  rows,
5329
- /* @__PURE__ */ jsx15(Text12, { children: " " })
6974
+ /* @__PURE__ */ jsx22(Text21, { children: " " })
5330
6975
  ] });
5331
6976
  }
5332
6977
 
5333
6978
  // src/components/StyledHelpHeader.tsx
5334
- import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
6979
+ import { jsx as jsx23, jsxs as jsxs22 } from "react/jsx-runtime";
5335
6980
  function StyledHelpHeader({
5336
6981
  sections
5337
6982
  }) {
5338
6983
  const descriptionColumnOffset = getDescriptionColumnOffset(sections);
5339
- return /* @__PURE__ */ jsxs13(
5340
- Box13,
6984
+ return /* @__PURE__ */ jsxs22(
6985
+ Box22,
5341
6986
  {
5342
6987
  flexDirection: "row",
5343
6988
  borderStyle: "single",
@@ -5345,23 +6990,23 @@ function StyledHelpHeader({
5345
6990
  paddingX: 1,
5346
6991
  paddingY: 1,
5347
6992
  children: [
5348
- /* @__PURE__ */ jsx16(Box13, { flexShrink: 0, width: descriptionColumnOffset, children: /* @__PURE__ */ jsx16(Zorb, { size: 20 }) }),
5349
- /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [
5350
- /* @__PURE__ */ jsx16(Text13, { bold: true, children: /* @__PURE__ */ jsxs13(Text13, { backgroundColor: "#3fff00", color: "black", children: [
6993
+ /* @__PURE__ */ jsx23(Box22, { flexShrink: 0, width: descriptionColumnOffset, children: /* @__PURE__ */ jsx23(Zorb, { size: 20 }) }),
6994
+ /* @__PURE__ */ jsxs22(Box22, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: [
6995
+ /* @__PURE__ */ jsx23(Text22, { bold: true, children: /* @__PURE__ */ jsxs22(Text22, { backgroundColor: "#3fff00", color: "black", children: [
5351
6996
  " ",
5352
6997
  "Zora CLI",
5353
6998
  " "
5354
6999
  ] }) }),
5355
- /* @__PURE__ */ jsxs13(Text13, { children: [
5356
- /* @__PURE__ */ jsx16(Text13, { dimColor: true, children: "Trade what's trending. Run" }),
7000
+ /* @__PURE__ */ jsxs22(Text22, { children: [
7001
+ /* @__PURE__ */ jsx23(Text22, { dimColor: true, children: "Trade what's trending. Run" }),
5357
7002
  " ",
5358
- /* @__PURE__ */ jsxs13(Text13, { backgroundColor: "#3fff00", color: "black", children: [
7003
+ /* @__PURE__ */ jsxs22(Text22, { backgroundColor: "#3fff00", color: "black", children: [
5359
7004
  " ",
5360
7005
  "zora setup",
5361
7006
  " "
5362
7007
  ] }),
5363
7008
  " ",
5364
- /* @__PURE__ */ jsx16(Text13, { dimColor: true, children: "to get started." })
7009
+ /* @__PURE__ */ jsx23(Text22, { dimColor: true, children: "to get started." })
5365
7010
  ] })
5366
7011
  ] })
5367
7012
  ]
@@ -5370,11 +7015,11 @@ function StyledHelpHeader({
5370
7015
  }
5371
7016
 
5372
7017
  // src/index.tsx
5373
- import { jsx as jsx17 } from "react/jsx-runtime";
7018
+ import { jsx as jsx24 } from "react/jsx-runtime";
5374
7019
  if (process.env.ZORA_API_TARGET) {
5375
7020
  setApiBaseUrl(process.env.ZORA_API_TARGET);
5376
7021
  }
5377
- var version = true ? "1.0.0" : JSON.parse(
7022
+ var version = true ? "1.1.0" : JSON.parse(
5378
7023
  readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
5379
7024
  ).version;
5380
7025
  function styledHelpWriteOut(showHeader) {
@@ -5382,8 +7027,8 @@ function styledHelpWriteOut(showHeader) {
5382
7027
  if (supportsTruecolor()) {
5383
7028
  const sections = parseHelpSections(str);
5384
7029
  if (sections.length > 0) {
5385
- const header = showHeader ? /* @__PURE__ */ jsx17(StyledHelpHeader, { sections }) : void 0;
5386
- renderOnce(/* @__PURE__ */ jsx17(StyledHelp, { sections, header }));
7030
+ const header = showHeader ? /* @__PURE__ */ jsx24(StyledHelpHeader, { sections }) : void 0;
7031
+ renderOnce(/* @__PURE__ */ jsx24(StyledHelp, { sections, header }));
5387
7032
  return;
5388
7033
  }
5389
7034
  }
@@ -5394,7 +7039,7 @@ function styledHelpWriteOut(showHeader) {
5394
7039
  };
5395
7040
  }
5396
7041
  var buildProgram = () => {
5397
- const program2 = new Command12().name("zora").description("Trade what's trending. Run `zora setup` to get started.").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
7042
+ const program2 = new Command11().name("zora").description("Trade what's trending. Run `zora setup` to get started.").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
5398
7043
  const helpWidth = (process.stdout.columns || 80) - 4;
5399
7044
  program2.configureHelp({
5400
7045
  helpWidth,
@@ -5412,7 +7057,6 @@ var buildProgram = () => {
5412
7057
  program2.addCommand(buyCommand);
5413
7058
  program2.addCommand(exploreCommand);
5414
7059
  program2.addCommand(getCommand);
5415
- program2.addCommand(priceHistoryCommand);
5416
7060
  program2.addCommand(profileCommand);
5417
7061
  program2.addCommand(setupCommand);
5418
7062
  program2.addCommand(walletCommand);
@@ -5431,7 +7075,7 @@ var buildProgram = () => {
5431
7075
  const expected = actionCommand.registeredArguments.length;
5432
7076
  if (expected > 0 && actionCommand.args.length === 0 && !argOptionalCommands.has(actionCommand.name())) {
5433
7077
  actionCommand.outputHelp();
5434
- process.exit(1);
7078
+ safeExit(ERROR);
5435
7079
  }
5436
7080
  });
5437
7081
  return program2;
@@ -5439,17 +7083,24 @@ var buildProgram = () => {
5439
7083
  var program = buildProgram();
5440
7084
  if (!process.env.VITEST) {
5441
7085
  identify();
7086
+ let exitCode = null;
5442
7087
  try {
5443
7088
  await program.parseAsync();
5444
7089
  } catch (err) {
5445
7090
  if (err instanceof ExitPromptError) {
5446
7091
  console.log("\nAborted.");
5447
- process.exit(0);
7092
+ exitCode = 0;
7093
+ } else if (err instanceof CliExitError) {
7094
+ exitCode = err.exitCode;
7095
+ } else {
7096
+ throw err;
5448
7097
  }
5449
- throw err;
5450
7098
  } finally {
5451
7099
  await shutdownAnalytics();
5452
7100
  }
7101
+ if (exitCode !== null) {
7102
+ process.exit(exitCode);
7103
+ }
5453
7104
  }
5454
7105
  export {
5455
7106
  buildProgram