hufi-cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +49 -35
  3. package/dist/cli.js +197 -43
  4. package/package.json +1 -1
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2024 hu-fi
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE F*** YOU WANT TO.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # hufi-cli
2
2
 
3
- CLI tool for the [hu.fi](https://hu.finance) DeFi platform.
3
+ [![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](http://www.wtfpl.net/about/)
4
+
5
+ CLI tool for the [hu.fi](https://hu.finance) platform.
4
6
 
5
7
  ## Install
6
8
 
@@ -14,19 +16,19 @@ Or run without installing:
14
16
  bunx hufi-cli <command>
15
17
  ```
16
18
 
17
- All examples below assume global install. Otherwise replace `hufi` with `bunx hufi-cli`.
19
+ All examples below assume global install. Otherwise replace `hufi-cli` with `bunx hufi-cli`.
18
20
 
19
21
  ## Quick Start
20
22
 
21
23
  ```bash
22
24
  # Generate a wallet
23
- hufi auth generate
25
+ hufi-cli auth generate
24
26
 
25
27
  # Login with saved key
26
- hufi auth login
28
+ hufi-cli auth login
27
29
 
28
30
  # Browse campaigns
29
- hufi campaign list
31
+ hufi-cli campaign list
30
32
  ```
31
33
 
32
34
  ## Commands
@@ -40,9 +42,9 @@ hufi campaign list
40
42
  | `auth status` | Show current auth status |
41
43
 
42
44
  ```bash
43
- hufi auth generate --json
44
- hufi auth login --private-key <key>
45
- hufi auth status
45
+ hufi-cli auth generate --json
46
+ hufi-cli auth login --private-key <key>
47
+ hufi-cli auth status
46
48
  ```
47
49
 
48
50
  ### campaign
@@ -59,15 +61,15 @@ hufi auth status
59
61
  | `campaign create` | Create a new campaign (launch escrow on-chain) |
60
62
 
61
63
  ```bash
62
- hufi campaign list # list active campaigns
63
- hufi campaign list --status completed --chain-id 1 # completed on Ethereum
64
- hufi campaign get --chain-id 137 --address 0x... # campaign details
65
- hufi campaign join --address 0x... # join (chain-id defaults to 137)
66
- hufi campaign status --address 0x... # check status
67
- hufi campaign progress --address 0x... # your progress
68
- hufi campaign progress --address 0x... --watch # live updates (polling)
69
- hufi campaign progress --address 0x... --watch --interval 3000
70
- hufi campaign leaderboard --address 0x... # leaderboard
64
+ hufi-cli campaign list # list active campaigns
65
+ hufi-cli campaign list --status completed --chain-id 1 # completed on Ethereum
66
+ hufi-cli campaign get --chain-id 137 --address 0x... # campaign details
67
+ hufi-cli campaign join --address 0x... # join (chain-id defaults to 137)
68
+ hufi-cli campaign status --address 0x... # check status
69
+ hufi-cli campaign progress --address 0x... # your progress
70
+ hufi-cli campaign progress --address 0x... --watch # live updates (polling)
71
+ hufi-cli campaign progress --address 0x... --watch --interval 3000
72
+ hufi-cli campaign leaderboard --address 0x... # leaderboard
71
73
  ```
72
74
 
73
75
  `campaign list` and `campaign get` print exact campaign timestamps and round token balances for human-readable text output.
@@ -76,23 +78,25 @@ hufi campaign leaderboard --address 0x... # leaderboard
76
78
 
77
79
  Requires staked HMT, gas, and fund tokens (USDT/USDC). Creates an escrow contract on-chain.
78
80
 
81
+ Before broadcasting, the CLI validates the campaign type-specific target, checks the 6-hour to 100-day duration window, verifies your minimum HMT stake when possible, inspects fund token balance, and estimates gas for approval plus creation.
82
+
79
83
  ```bash
80
84
  # Market Making
81
- hufi campaign create \
85
+ hufi-cli campaign create \
82
86
  --type market_making --exchange mexc --symbol HMT/USDT \
83
87
  --start-date 2026-04-01 --end-date 2026-05-01 \
84
88
  --fund-token USDT --fund-amount 10000 \
85
89
  --daily-volume-target 50000
86
90
 
87
91
  # Holding
88
- hufi campaign create \
92
+ hufi-cli campaign create \
89
93
  --type holding --exchange mexc --symbol HMT \
90
94
  --start-date 2026-04-01 --end-date 2026-05-01 \
91
95
  --fund-token USDT --fund-amount 5000 \
92
96
  --daily-balance-target 1000
93
97
 
94
98
  # Threshold
95
- hufi campaign create \
99
+ hufi-cli campaign create \
96
100
  --type threshold --exchange mexc --symbol HMT \
97
101
  --start-date 2026-04-01 --end-date 2026-05-01 \
98
102
  --fund-token USDT --fund-amount 5000 \
@@ -111,12 +115,17 @@ Running `campaign status/join/progress/leaderboard` without `-a` shows help.
111
115
  | `exchange revalidate` | Revalidate an exchange API key |
112
116
 
113
117
  ```bash
114
- hufi exchange register --name mexc --api-key <key> --secret-key <secret>
115
- hufi exchange list
116
- hufi exchange revalidate --name mexc
117
- hufi exchange delete --name mexc
118
+ hufi-cli exchange register --name mexc --api-key <key> --secret-key <secret>
119
+ hufi-cli exchange register --name bitmart --api-key <key> --secret-key <secret> --bitmart-memo <memo>
120
+ hufi-cli exchange list
121
+ hufi-cli exchange revalidate mexc
122
+ hufi-cli exchange delete mexc
118
123
  ```
119
124
 
125
+ `exchange register` expects the CCXT exchange name in `--name` and accepts `--bitmart-memo` for Bitmart accounts that require an extra memo value.
126
+
127
+ You must run `hufi-cli auth login` before `exchange register`, `exchange list`, `exchange delete`, or `exchange revalidate`.
128
+
120
129
  ### staking
121
130
 
122
131
  | Command | Description |
@@ -128,12 +137,12 @@ hufi exchange delete --name mexc
128
137
  | `staking withdraw` | Withdraw unlocked tokens after lock period |
129
138
 
130
139
  ```bash
131
- hufi staking deposit # show address QR code
132
- hufi staking status # check your staking
133
- hufi staking status --address 0x... # check another address
134
- hufi staking stake -a 1000 # stake 1000 HMT
135
- hufi staking unstake -a 500 # unstake 500 HMT
136
- hufi staking withdraw # withdraw unlocked tokens
140
+ hufi-cli staking deposit # show address QR code
141
+ hufi-cli staking status # check your staking
142
+ hufi-cli staking status --address 0x... # check another address
143
+ hufi-cli staking stake 1000 # stake 1000 HMT
144
+ hufi-cli staking unstake 500 # unstake 500 HMT
145
+ hufi-cli staking withdraw # withdraw unlocked tokens
137
146
  ```
138
147
 
139
148
  Supports Polygon (chain 137) and Ethereum (chain 1). Staking contract: `0x01D1...07F1D` on Polygon.
@@ -143,10 +152,10 @@ Supports Polygon (chain 137) and Ethereum (chain 1). Staking contract: `0x01D1..
143
152
  Portfolio overview — staking, active campaigns, and progress in one view.
144
153
 
145
154
  ```bash
146
- hufi dashboard # full overview
147
- hufi dashboard --json # machine output
148
- hufi dashboard --export csv # export active campaign rows as CSV
149
- hufi dashboard --export json
155
+ hufi-cli dashboard # full overview
156
+ hufi-cli dashboard --json # machine output
157
+ hufi-cli dashboard --export csv # export active campaign rows as CSV
158
+ hufi-cli dashboard --export json
150
159
  ```
151
160
 
152
161
  ## Global Options
@@ -158,7 +167,7 @@ hufi dashboard --export json
158
167
  | `-V, --version` | Show version |
159
168
  | `-h, --help` | Show help |
160
169
 
161
- All commands support `--json` for machine-readable output.
170
+ Most command actions support `--json` for machine-readable output. Help output remains text, and commands that intentionally show help when required arguments are missing do not emit a JSON error envelope.
162
171
 
163
172
  ## Configuration
164
173
 
@@ -181,6 +190,7 @@ bun install # install deps
181
190
  bun run dev -- --help # run from source
182
191
  bun run build # build to dist/cli.js
183
192
  bun test # unit tests
193
+ ./test-cli.sh # full CLI integration coverage
184
194
  bun run test:cli # integration tests
185
195
  bun run typecheck # type check
186
196
  ```
@@ -191,3 +201,7 @@ bun run typecheck # type check
191
201
  |---------|-----|
192
202
  | Recording Oracle | https://ro.hu.finance |
193
203
  | Campaign Launcher | https://cl.hu.finance |
204
+
205
+ ## License
206
+
207
+ This project is released under the [DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE v2](http://www.wtfpl.net/about/). Do whatever you want with it.
package/dist/cli.js CHANGED
@@ -22901,6 +22901,20 @@ function validateConfig(config) {
22901
22901
  if (config.address !== undefined && !isEvmAddress(config.address)) {
22902
22902
  issues.push("address must be a valid 0x-prefixed EVM address");
22903
22903
  }
22904
+ if (config.rpcUrls !== undefined) {
22905
+ if (typeof config.rpcUrls !== "object" || config.rpcUrls === null || Array.isArray(config.rpcUrls)) {
22906
+ issues.push("rpcUrls must be an object mapping chain IDs to RPC URLs");
22907
+ } else {
22908
+ for (const [chainId, url] of Object.entries(config.rpcUrls)) {
22909
+ if (!/^\d+$/.test(chainId)) {
22910
+ issues.push(`rpcUrls key '${chainId}' must be a numeric chain ID`);
22911
+ }
22912
+ if (typeof url !== "string" || !isHttpUrl(url)) {
22913
+ issues.push(`rpcUrls.${chainId} must be a valid http/https URL`);
22914
+ }
22915
+ }
22916
+ }
22917
+ }
22904
22918
  return {
22905
22919
  valid: issues.length === 0,
22906
22920
  issues
@@ -22928,7 +22942,7 @@ function createAuthCommand() {
22928
22942
  auth.command("login").description("Authenticate with Recording Oracle using a private key").option("-k, --private-key <key>", "EVM private key (uses saved key if not provided)").option("-u, --api-url <url>", "Recording Oracle API URL").option("--json", "Output as JSON").action(async (opts) => {
22929
22943
  const privateKey = opts.privateKey ?? loadKey();
22930
22944
  if (!privateKey) {
22931
- printText("No private key provided. Run: hufi auth login -k <key> or hufi auth generate");
22945
+ printText("No private key provided. Run: hufi-cli auth login -k <key> or hufi-cli auth generate");
22932
22946
  process.exit(1);
22933
22947
  }
22934
22948
  const config = loadConfig();
@@ -22945,7 +22959,8 @@ function createAuthCommand() {
22945
22959
  printJson({ address: result.address, accessToken: result.accessToken });
22946
22960
  } else {
22947
22961
  printText(`Authenticated as ${result.address}`);
22948
- printText(`Token saved to ${getConfigPath()}`);
22962
+ printText(`Private key loaded from ${getKeyPath()}`);
22963
+ printText(`Tokens saved to ${getConfigPath()}`);
22949
22964
  }
22950
22965
  } catch (err) {
22951
22966
  const message = err instanceof Error ? err.message : String(err);
@@ -22994,7 +23009,7 @@ function createAuthCommand() {
22994
23009
  printText(`Authenticated as ${status.address}`);
22995
23010
  printText(`API: ${status.apiUrl}`);
22996
23011
  } else {
22997
- printText("Not authenticated. Run: hufi auth login --private-key <key>");
23012
+ printText("Not authenticated. Run: hufi-cli auth login -k <key>");
22998
23013
  }
22999
23014
  }
23000
23015
  });
@@ -23063,9 +23078,33 @@ function requireAuthAddress() {
23063
23078
  }
23064
23079
 
23065
23080
  // src/commands/exchange.ts
23081
+ var authHintByAction = {
23082
+ register: "Run: hufi auth login --private-key <key> before registering exchange API keys.",
23083
+ list: "Run: hufi auth login --private-key <key> before listing exchange API keys.",
23084
+ delete: "Run: hufi auth login --private-key <key> before deleting exchange API keys.",
23085
+ revalidate: "Run: hufi auth login --private-key <key> before revalidating exchange API keys."
23086
+ };
23087
+ function isUnauthorizedError(err) {
23088
+ return err instanceof ApiError && err.status === 401;
23089
+ }
23090
+ function formatExchangeCommandErrorMessage(action, err) {
23091
+ const message = err instanceof Error ? err.message : String(err);
23092
+ const commandTarget = action === "list" ? "exchange API keys" : "exchange API key";
23093
+ if (isUnauthorizedError(err)) {
23094
+ return `Failed to ${action} ${commandTarget}: ${message}. ${authHintByAction[action]}`;
23095
+ }
23096
+ return `Failed to ${action} ${commandTarget}: ${message}`;
23097
+ }
23098
+ function formatRevalidateText(exchangeName, result) {
23099
+ const lines = [`${exchangeName}: ${result.is_valid ? "valid" : "invalid"}`];
23100
+ if ((result.missing_permissions?.length ?? 0) > 0) {
23101
+ lines.push(`Missing permissions: ${result.missing_permissions?.join(", ")}`);
23102
+ }
23103
+ return lines;
23104
+ }
23066
23105
  function createExchangeCommand() {
23067
23106
  const exchange = new Command("exchange").description("Exchange API key management");
23068
- exchange.command("register").description("Register a read-only exchange API key").requiredOption("-n, --name <name>", "Exchange name (e.g. binance, mexc)").requiredOption("--api-key <key>", "Read-only API key").requiredOption("--secret-key <key>", "Read-only API secret").option("--bitmart-memo <memo>", "Bitmart memo (only for bitmart)").option("--json", "Output as JSON").action(async (opts) => {
23107
+ exchange.command("register").description("Register a read-only exchange API key").requiredOption("-n, --name <name>", "CCXT exchange name (e.g. binance, mexc)").requiredOption("--api-key <key>", "Read-only API key").requiredOption("--secret-key <key>", "Read-only API secret").option("--bitmart-memo <memo>", "Bitmart memo (only for bitmart)").option("--json", "Output as JSON").action(async (opts) => {
23069
23108
  const { baseUrl, accessToken } = requireAuthToken();
23070
23109
  try {
23071
23110
  const result = await registerExchangeApiKey(baseUrl, accessToken, opts.name, opts.apiKey, opts.secretKey, opts.bitmartMemo);
@@ -23078,8 +23117,7 @@ function createExchangeCommand() {
23078
23117
  }
23079
23118
  }
23080
23119
  } catch (err) {
23081
- const message = err instanceof Error ? err.message : String(err);
23082
- printText(`Failed to register exchange API key: ${message}`);
23120
+ printText(formatExchangeCommandErrorMessage("register", err));
23083
23121
  process.exitCode = 1;
23084
23122
  }
23085
23123
  });
@@ -23100,42 +23138,49 @@ function createExchangeCommand() {
23100
23138
  }
23101
23139
  }
23102
23140
  } catch (err) {
23103
- const message = err instanceof Error ? err.message : String(err);
23104
- printText(`Failed to list exchange API keys: ${message}`);
23141
+ printText(formatExchangeCommandErrorMessage("list", err));
23105
23142
  process.exitCode = 1;
23106
23143
  }
23107
23144
  });
23108
- exchange.command("delete").description("Delete API keys for an exchange").requiredOption("-n, --name <name>", "Exchange name (e.g. mexc, bybit)").option("--json", "Output as JSON").action(async (opts) => {
23145
+ exchange.command("delete [name]").description("Delete API keys for an exchange").usage("[name] [options]").option("-n, --name <name>", "Exchange name (e.g. mexc, bybit)").option("--json", "Output as JSON").action(async (nameArg, opts) => {
23146
+ const exchangeName = nameArg ?? opts.name;
23147
+ if (!exchangeName) {
23148
+ printText("Exchange name is required. Usage: hufi exchange delete <name>");
23149
+ process.exitCode = 1;
23150
+ return;
23151
+ }
23109
23152
  const { baseUrl, accessToken } = requireAuthToken();
23110
23153
  try {
23111
- await deleteExchangeApiKey(baseUrl, accessToken, opts.name);
23154
+ await deleteExchangeApiKey(baseUrl, accessToken, exchangeName);
23112
23155
  if (opts.json) {
23113
- printJson({ deleted: true, exchange_name: opts.name });
23156
+ printJson({ deleted: true, exchange_name: exchangeName });
23114
23157
  } else {
23115
- printText(`Deleted API keys for ${opts.name}.`);
23158
+ printText(`Deleted API keys for ${exchangeName}.`);
23116
23159
  }
23117
23160
  } catch (err) {
23118
- const message = err instanceof Error ? err.message : String(err);
23119
- printText(`Failed to delete exchange API keys: ${message}`);
23161
+ printText(formatExchangeCommandErrorMessage("delete", err));
23120
23162
  process.exitCode = 1;
23121
23163
  }
23122
23164
  });
23123
- exchange.command("revalidate").description("Revalidate exchange API key").requiredOption("-n, --name <name>", "Exchange name (e.g. mexc, bybit)").option("--json", "Output as JSON").action(async (opts) => {
23165
+ exchange.command("revalidate [name]").description("Revalidate exchange API key").usage("[name] [options]").option("-n, --name <name>", "Exchange name (e.g. mexc, bybit)").option("--json", "Output as JSON").action(async (nameArg, opts) => {
23166
+ const exchangeName = nameArg ?? opts.name;
23167
+ if (!exchangeName) {
23168
+ printText("Exchange name is required. Usage: hufi exchange revalidate <name>");
23169
+ process.exitCode = 1;
23170
+ return;
23171
+ }
23124
23172
  const { baseUrl, accessToken } = requireAuthToken();
23125
23173
  try {
23126
- const result = await revalidateExchangeApiKey(baseUrl, accessToken, opts.name);
23174
+ const result = await revalidateExchangeApiKey(baseUrl, accessToken, exchangeName);
23127
23175
  if (opts.json) {
23128
23176
  printJson(result);
23129
23177
  } else {
23130
- const status = result.is_valid ? "valid" : "invalid";
23131
- printText(`${opts.name}: ${status}`);
23132
- if (result.missing_permissions.length > 0) {
23133
- printText(`Missing permissions: ${result.missing_permissions.join(", ")}`);
23178
+ for (const line of formatRevalidateText(exchangeName, result)) {
23179
+ printText(line);
23134
23180
  }
23135
23181
  }
23136
23182
  } catch (err) {
23137
- const message = err instanceof Error ? err.message : String(err);
23138
- printText(`Failed to revalidate exchange API key: ${message}`);
23183
+ printText(formatExchangeCommandErrorMessage("revalidate", err));
23139
23184
  process.exitCode = 1;
23140
23185
  }
23141
23186
  });
@@ -24711,6 +24756,14 @@ function getContracts(chainId) {
24711
24756
  return c;
24712
24757
  }
24713
24758
  function getRpc(chainId) {
24759
+ const override = process.env[`HUFI_RPC_${chainId}`];
24760
+ if (override) {
24761
+ return override;
24762
+ }
24763
+ const configOverride = loadConfig().rpcUrls?.[String(chainId)];
24764
+ if (configOverride) {
24765
+ return configOverride;
24766
+ }
24714
24767
  const rpcs = RPC_URLS[chainId];
24715
24768
  if (!rpcs) {
24716
24769
  throw new Error(`No RPC URLs for chain ${chainId}`);
@@ -24971,6 +25024,56 @@ function formatCampaignTimestamp(value) {
24971
25024
  return "-";
24972
25025
  return value.replace("T", " ").replace(/\.\d+Z$/, "").replace(/Z$/, "");
24973
25026
  }
25027
+ function formatUtcTimestamp(value) {
25028
+ if (typeof value !== "string")
25029
+ return "-";
25030
+ const date = new Date(value);
25031
+ if (Number.isNaN(date.getTime()))
25032
+ return String(value);
25033
+ return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
25034
+ }
25035
+ function formatMetricValue(value) {
25036
+ if (value === null || value === undefined)
25037
+ return "-";
25038
+ if (typeof value === "number") {
25039
+ return Number.isInteger(value) ? String(value) : value.toFixed(4).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
25040
+ }
25041
+ if (typeof value === "string") {
25042
+ const trimmed = value.trim();
25043
+ if (trimmed && /^-?\d+(\.\d+)?$/.test(trimmed)) {
25044
+ const num = Number(trimmed);
25045
+ if (!Number.isNaN(num)) {
25046
+ return formatMetricValue(num);
25047
+ }
25048
+ }
25049
+ return value;
25050
+ }
25051
+ return JSON.stringify(value);
25052
+ }
25053
+ function printAlignedMetric(label, value, labelWidth = 14) {
25054
+ printText(` ${label.padEnd(labelWidth)} ${formatMetricValue(value)}`);
25055
+ }
25056
+ function printCampaignProgressCard(result) {
25057
+ const myMeta = result.my_meta && typeof result.my_meta === "object" ? result.my_meta : {};
25058
+ const totalMeta = result.total_meta && typeof result.total_meta === "object" ? result.total_meta : {};
25059
+ const myScore = Number(result.my_score ?? 0);
25060
+ const totalScore = Number(totalMeta.total_score ?? 0);
25061
+ const scoreShare = totalScore > 0 && Number.isFinite(myScore) ? `${(myScore / totalScore * 100).toFixed(2)}%` : "-";
25062
+ printText("Campaign Progress");
25063
+ printText("-----------------");
25064
+ printText("[Window]");
25065
+ printAlignedMetric("From", formatUtcTimestamp(result.from));
25066
+ printAlignedMetric("To", formatUtcTimestamp(result.to));
25067
+ printText("");
25068
+ printText("[Mine]");
25069
+ printAlignedMetric("Score", result.my_score);
25070
+ printAlignedMetric("Token balance", myMeta.token_balance);
25071
+ printAlignedMetric("Score share", scoreShare);
25072
+ printText("");
25073
+ printText("[Totals]");
25074
+ printAlignedMetric("Total score", totalMeta.total_score);
25075
+ printAlignedMetric("Total balance", totalMeta.total_balance);
25076
+ }
24974
25077
  function formatTokenAmount(value, decimals, displayDecimals = 2) {
24975
25078
  const amount = new bignumber_default(value).dividedBy(new bignumber_default(10).pow(decimals));
24976
25079
  const rounded = amount.decimalPlaces(displayDecimals, bignumber_default.ROUND_HALF_UP);
@@ -24986,6 +25089,34 @@ function getLauncherUrl() {
24986
25089
  const config = loadConfig();
24987
25090
  return (config.launcherApiUrl ?? "https://cl.hu.finance").replace(/\/+$/, "");
24988
25091
  }
25092
+ function printCampaignSummary(campaign, options = {}) {
25093
+ const indent = options.indent ?? "";
25094
+ const joinedTag = options.joinedTag ?? "";
25095
+ const showLauncher = options.showLauncher ?? true;
25096
+ const exchange = String(campaign.exchange_name ?? "?");
25097
+ const symbol = String(campaign.symbol ?? "?");
25098
+ const type = String(campaign.type ?? "?");
25099
+ const chainId = String(campaign.chain_id ?? "-");
25100
+ const address = String(campaign.address ?? campaign.escrow_address ?? "-");
25101
+ const status = String(campaign.status ?? "-");
25102
+ const startDate = typeof campaign.start_date === "string" ? campaign.start_date : undefined;
25103
+ const endDate = typeof campaign.end_date === "string" ? campaign.end_date : undefined;
25104
+ const launcher = typeof campaign.launcher === "string" ? campaign.launcher : undefined;
25105
+ const decimals = Number(campaign.fund_token_decimals ?? 0);
25106
+ const fundAmount = new bignumber_default(String(campaign.fund_amount ?? 0));
25107
+ const balanceNum = new bignumber_default(String(campaign.balance ?? 0));
25108
+ const pct = fundAmount.gt(0) ? balanceNum.dividedBy(fundAmount).times(100).toFixed(1) : "0.0";
25109
+ const fundTokenSymbol = String(campaign.fund_token_symbol ?? campaign.fund_token ?? "-");
25110
+ printText(`${indent}${exchange} ${symbol} (${type})${joinedTag}`);
25111
+ printText(`${indent} chain: ${chainId}`);
25112
+ printText(`${indent} address: ${address}`);
25113
+ printText(`${indent} status: ${status}`);
25114
+ printText(`${indent} duration: ${formatCampaignTimestamp(startDate)} ~ ${formatCampaignTimestamp(endDate)}`);
25115
+ printText(`${indent} funded: ${formatTokenAmount(String(campaign.fund_amount ?? 0), decimals)} ${fundTokenSymbol} paid: ${formatTokenAmount(String(campaign.amount_paid ?? 0), decimals)} balance: ${formatTokenAmount(String(campaign.balance ?? 0), decimals)} (${pct}%)`);
25116
+ if (showLauncher && launcher) {
25117
+ printText(`${indent} launcher: ${launcher}`);
25118
+ }
25119
+ }
24989
25120
  function createCampaignCommand() {
24990
25121
  const campaign = new Command("campaign").description("Campaign management commands");
24991
25122
  campaign.command("list").description("List available campaigns").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("-s, --status <status>", "Filter by status (active, completed, cancelled, to_cancel)", "active").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("-l, --limit <n>", "Max results", Number, 20).option("--json", "Output as JSON").action(async (opts) => {
@@ -25017,16 +25148,10 @@ function createCampaignCommand() {
25017
25148
  const key = `${c.chain_id}:${c.address}`;
25018
25149
  const joined = joinedKeys.has(key);
25019
25150
  const tag = joined ? " [JOINED]" : "";
25020
- const decimals = c.fund_token_decimals ?? 0;
25021
- const fundAmount = new bignumber_default(c.fund_amount);
25022
- const balanceNum = new bignumber_default(c.balance);
25023
- const pct = fundAmount.gt(0) ? balanceNum.dividedBy(fundAmount).times(100).toFixed(1) : "0.0";
25024
- printText(` ${c.exchange_name} ${c.symbol} (${c.type})${tag}`);
25025
- printText(` chain: ${c.chain_id}`);
25026
- printText(` address: ${c.address}`);
25027
- printText(` status: ${c.status}`);
25028
- printText(` duration: ${formatCampaignTimestamp(c.start_date)} ~ ${formatCampaignTimestamp(c.end_date)}`);
25029
- printText(` funded: ${formatTokenAmount(c.fund_amount, decimals)} ${c.fund_token_symbol} paid: ${formatTokenAmount(c.amount_paid, decimals)} balance: ${formatTokenAmount(c.balance, decimals)} (${pct}%)`);
25151
+ printCampaignSummary(c, {
25152
+ indent: " ",
25153
+ joinedTag: tag
25154
+ });
25030
25155
  printText("");
25031
25156
  }
25032
25157
  if (opts.status === "active") {
@@ -25078,10 +25203,24 @@ function createCampaignCommand() {
25078
25203
  if (campaigns.length === 0) {
25079
25204
  printText("No joined campaigns found.");
25080
25205
  } else {
25081
- printText(`Joined campaigns (${campaigns.length}):`);
25206
+ printText(`Joined campaigns (${campaigns.length}):
25207
+ `);
25082
25208
  for (const c of campaigns) {
25083
- const label = c.campaign_name ?? c.name ?? c.id;
25084
- printText(` - ${label}`);
25209
+ const record = c;
25210
+ const hasListMetadata = Boolean(record.exchange_name ?? record.symbol ?? record.type ?? record.address ?? record.escrow_address);
25211
+ if (hasListMetadata) {
25212
+ printCampaignSummary(record, {
25213
+ indent: " ",
25214
+ showLauncher: typeof record.launcher === "string"
25215
+ });
25216
+ } else {
25217
+ const exchange = String(record.exchange_name ?? "").trim();
25218
+ const symbol = String(record.symbol ?? "").trim();
25219
+ const exchangeSymbol = [exchange, symbol].filter(Boolean).join(" ");
25220
+ const label = record.campaign_name ?? record.name ?? (exchangeSymbol || undefined) ?? record.address ?? record.escrow_address ?? c.id ?? "(unnamed campaign)";
25221
+ printText(` - ${label}`);
25222
+ }
25223
+ printText("");
25085
25224
  }
25086
25225
  }
25087
25226
  }
@@ -25171,9 +25310,12 @@ function createCampaignCommand() {
25171
25310
  const r = result;
25172
25311
  if (r.message) {
25173
25312
  printText(String(r.message));
25313
+ } else if ("from" in r || "to" in r || "my_score" in r || "my_meta" in r || "total_meta" in r) {
25314
+ printCampaignProgressCard(r);
25174
25315
  } else {
25175
25316
  for (const [key, value] of Object.entries(r)) {
25176
- printText(` ${key}: ${value}`);
25317
+ const displayValue = value !== null && typeof value === "object" ? JSON.stringify(value) : String(value);
25318
+ printText(` ${key}: ${displayValue}`);
25177
25319
  }
25178
25320
  }
25179
25321
  }
@@ -25406,11 +25548,17 @@ function createStakingCommand() {
25406
25548
  process.exitCode = 1;
25407
25549
  }
25408
25550
  });
25409
- staking.command("stake").description("Stake HMT tokens").requiredOption("-a, --amount <amount>", "Amount of HMT to stake").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--json", "Output as JSON").action(async (opts) => {
25551
+ staking.command("stake [amount]").description("Stake HMT tokens").usage("[amount] [options]").option("-a, --amount <amount>", "Amount of HMT to stake").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--json", "Output as JSON").action(async (amountArg, opts) => {
25552
+ const amount = amountArg ?? opts.amount;
25553
+ if (!amount) {
25554
+ printText("Amount is required. Usage: hufi staking stake <amount>");
25555
+ process.exitCode = 1;
25556
+ return;
25557
+ }
25410
25558
  const privateKey = requireKey();
25411
25559
  try {
25412
- printText(`Staking ${opts.amount} HMT on chain ${opts.chainId}...`);
25413
- const hash2 = await stakeHMT(privateKey, opts.amount, opts.chainId);
25560
+ printText(`Staking ${amount} HMT on chain ${opts.chainId}...`);
25561
+ const hash2 = await stakeHMT(privateKey, amount, opts.chainId);
25414
25562
  if (opts.json) {
25415
25563
  printJson({ txHash: hash2 });
25416
25564
  } else {
@@ -25423,11 +25571,17 @@ function createStakingCommand() {
25423
25571
  process.exitCode = 1;
25424
25572
  }
25425
25573
  });
25426
- staking.command("unstake").description("Initiate unstaking (tokens will be locked for the lock period)").requiredOption("-a, --amount <amount>", "Amount of HMT to unstake").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--json", "Output as JSON").action(async (opts) => {
25574
+ staking.command("unstake [amount]").description("Initiate unstaking (tokens will be locked for the lock period)").usage("[amount] [options]").option("-a, --amount <amount>", "Amount of HMT to unstake").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--json", "Output as JSON").action(async (amountArg, opts) => {
25575
+ const amount = amountArg ?? opts.amount;
25576
+ if (!amount) {
25577
+ printText("Amount is required. Usage: hufi staking unstake <amount>");
25578
+ process.exitCode = 1;
25579
+ return;
25580
+ }
25427
25581
  const privateKey = requireKey();
25428
25582
  try {
25429
- printText(`Unstaking ${opts.amount} HMT on chain ${opts.chainId}...`);
25430
- const hash2 = await unstakeHMT(privateKey, opts.amount, opts.chainId);
25583
+ printText(`Unstaking ${amount} HMT on chain ${opts.chainId}...`);
25584
+ const hash2 = await unstakeHMT(privateKey, amount, opts.chainId);
25431
25585
  if (opts.json) {
25432
25586
  printJson({ txHash: hash2 });
25433
25587
  } else {
@@ -25600,7 +25754,7 @@ function createDashboardCommand() {
25600
25754
 
25601
25755
  // src/cli.ts
25602
25756
  var program2 = new Command;
25603
- program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("1.0.1").option("--config-file <path>", "Custom config file path (default: ~/.hufi-cli/config.json)").option("--key-file <path>", "Custom key file path (default: ~/.hufi-cli/key.json)").hook("preAction", (thisCommand) => {
25757
+ program2.name("hufi").description("CLI tool for Hu.fi platform").version("1.0.3").option("--config-file <path>", "Custom config file path (default: ~/.hufi-cli/config.json)").option("--key-file <path>", "Custom key file path (default: ~/.hufi-cli/key.json)").hook("preAction", (thisCommand) => {
25604
25758
  const opts = thisCommand.opts();
25605
25759
  if (opts.configFile) {
25606
25760
  setConfigFile(opts.configFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hufi-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hufi": "./dist/cli.js"