hufi-cli 0.8.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +419 -106
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,9 +65,13 @@ hufi campaign get --chain-id 137 --address 0x... # campaign details
65
65
  hufi campaign join --address 0x... # join (chain-id defaults to 137)
66
66
  hufi campaign status --address 0x... # check status
67
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
68
70
  hufi campaign leaderboard --address 0x... # leaderboard
69
71
  ```
70
72
 
73
+ `campaign list` and `campaign get` print exact campaign timestamps and round token balances for human-readable text output.
74
+
71
75
  #### Campaign Create
72
76
 
73
77
  Requires staked HMT, gas, and fund tokens (USDT/USDC). Creates an escrow contract on-chain.
@@ -141,6 +145,8 @@ Portfolio overview — staking, active campaigns, and progress in one view.
141
145
  ```bash
142
146
  hufi dashboard # full overview
143
147
  hufi dashboard --json # machine output
148
+ hufi dashboard --export csv # export active campaign rows as CSV
149
+ hufi dashboard --export json
144
150
  ```
145
151
 
146
152
  ## Global Options
package/dist/cli.js CHANGED
@@ -22697,28 +22697,62 @@ class ApiError extends Error {
22697
22697
  }
22698
22698
 
22699
22699
  // src/lib/http.ts
22700
+ function sleep(ms) {
22701
+ return new Promise((resolve) => setTimeout(resolve, ms));
22702
+ }
22703
+ function isRetryableStatus(status) {
22704
+ return status === 408 || status === 429 || status >= 500;
22705
+ }
22706
+ function resolveMessage(payload, status) {
22707
+ let message = `HTTP ${status}`;
22708
+ if (payload && typeof payload === "object" && "message" in payload) {
22709
+ message = String(payload.message);
22710
+ } else if (typeof payload === "string" && payload) {
22711
+ message = payload;
22712
+ }
22713
+ return message;
22714
+ }
22700
22715
  async function requestJson(url, options = {}) {
22701
- const response = await fetch(url, {
22702
- method: options.method ?? "GET",
22703
- headers: {
22704
- "Content-Type": "application/json",
22705
- ...options.headers ?? {}
22706
- },
22707
- body: options.body
22708
- });
22709
- const contentType = response.headers.get("content-type") ?? "";
22710
- const isJson = contentType.includes("application/json");
22711
- const payload = isJson ? await response.json() : await response.text();
22712
- if (!response.ok) {
22713
- let message = `HTTP ${response.status}`;
22714
- if (payload && typeof payload === "object" && "message" in payload) {
22715
- message = String(payload.message);
22716
- } else if (typeof payload === "string" && payload) {
22717
- message = payload;
22716
+ const retries = options.retry?.retries ?? 2;
22717
+ const initialDelayMs = options.retry?.initialDelayMs ?? 250;
22718
+ const maxDelayMs = options.retry?.maxDelayMs ?? 3000;
22719
+ let attempt = 0;
22720
+ while (true) {
22721
+ try {
22722
+ const response = await fetch(url, {
22723
+ method: options.method ?? "GET",
22724
+ headers: {
22725
+ "Content-Type": "application/json",
22726
+ ...options.headers ?? {}
22727
+ },
22728
+ body: options.body
22729
+ });
22730
+ const contentType = response.headers.get("content-type") ?? "";
22731
+ const isJson = contentType.includes("application/json");
22732
+ const payload = isJson ? await response.json() : await response.text();
22733
+ if (!response.ok) {
22734
+ const message = resolveMessage(payload, response.status);
22735
+ if (isRetryableStatus(response.status) && attempt < retries) {
22736
+ const delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
22737
+ attempt += 1;
22738
+ await sleep(delay);
22739
+ continue;
22740
+ }
22741
+ throw new ApiError(message, response.status, payload);
22742
+ }
22743
+ return payload;
22744
+ } catch (err) {
22745
+ if (err instanceof ApiError) {
22746
+ throw err;
22747
+ }
22748
+ if (attempt >= retries) {
22749
+ throw err;
22750
+ }
22751
+ const delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
22752
+ attempt += 1;
22753
+ await sleep(delay);
22718
22754
  }
22719
- throw new ApiError(message, response.status, payload);
22720
22755
  }
22721
- return payload;
22722
22756
  }
22723
22757
  function authHeaders(accessToken) {
22724
22758
  return { Authorization: `Bearer ${accessToken}` };
@@ -22840,6 +22874,38 @@ function loadKey() {
22840
22874
  return null;
22841
22875
  }
22842
22876
  }
22877
+ function isHttpUrl(value) {
22878
+ try {
22879
+ const parsed = new URL(value);
22880
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
22881
+ } catch {
22882
+ return false;
22883
+ }
22884
+ }
22885
+ function isEvmAddress(value) {
22886
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
22887
+ }
22888
+ function validateConfig(config) {
22889
+ const issues = [];
22890
+ if (config.recordingApiUrl !== undefined && !isHttpUrl(config.recordingApiUrl)) {
22891
+ issues.push("recordingApiUrl must be a valid http/https URL");
22892
+ }
22893
+ if (config.launcherApiUrl !== undefined && !isHttpUrl(config.launcherApiUrl)) {
22894
+ issues.push("launcherApiUrl must be a valid http/https URL");
22895
+ }
22896
+ if (config.defaultChainId !== undefined) {
22897
+ if (!Number.isInteger(config.defaultChainId) || config.defaultChainId <= 0) {
22898
+ issues.push("defaultChainId must be a positive integer");
22899
+ }
22900
+ }
22901
+ if (config.address !== undefined && !isEvmAddress(config.address)) {
22902
+ issues.push("address must be a valid 0x-prefixed EVM address");
22903
+ }
22904
+ return {
22905
+ valid: issues.length === 0,
22906
+ issues
22907
+ };
22908
+ }
22843
22909
 
22844
22910
  // src/lib/output.ts
22845
22911
  function printJson(data) {
@@ -22971,8 +23037,8 @@ async function revalidateExchangeApiKey(baseUrl, accessToken, exchangeName) {
22971
23037
  });
22972
23038
  }
22973
23039
 
22974
- // src/commands/exchange.ts
22975
- function requireAuth() {
23040
+ // src/lib/require-auth.ts
23041
+ function requireAuthToken() {
22976
23042
  const config = loadConfig();
22977
23043
  if (!config.accessToken) {
22978
23044
  printText("Not authenticated. Run: hufi auth login --private-key <key>");
@@ -22983,10 +23049,24 @@ function requireAuth() {
22983
23049
  accessToken: config.accessToken
22984
23050
  };
22985
23051
  }
23052
+ function requireAuthAddress() {
23053
+ const config = loadConfig();
23054
+ if (!config.accessToken || !config.address) {
23055
+ printText("Not authenticated. Run: hufi auth login --private-key <key>");
23056
+ process.exit(1);
23057
+ }
23058
+ return {
23059
+ baseUrl: config.recordingApiUrl.replace(/\/+$/, ""),
23060
+ accessToken: config.accessToken,
23061
+ address: config.address
23062
+ };
23063
+ }
23064
+
23065
+ // src/commands/exchange.ts
22986
23066
  function createExchangeCommand() {
22987
23067
  const exchange = new Command("exchange").description("Exchange API key management");
22988
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) => {
22989
- const { baseUrl, accessToken } = requireAuth();
23069
+ const { baseUrl, accessToken } = requireAuthToken();
22990
23070
  try {
22991
23071
  const result = await registerExchangeApiKey(baseUrl, accessToken, opts.name, opts.apiKey, opts.secretKey, opts.bitmartMemo);
22992
23072
  if (opts.json) {
@@ -23004,7 +23084,7 @@ function createExchangeCommand() {
23004
23084
  }
23005
23085
  });
23006
23086
  exchange.command("list").description("List registered exchange API keys").option("--json", "Output as JSON").action(async (opts) => {
23007
- const { baseUrl, accessToken } = requireAuth();
23087
+ const { baseUrl, accessToken } = requireAuthToken();
23008
23088
  try {
23009
23089
  const keys = await listExchangeApiKeys(baseUrl, accessToken);
23010
23090
  if (opts.json) {
@@ -23026,7 +23106,7 @@ function createExchangeCommand() {
23026
23106
  }
23027
23107
  });
23028
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) => {
23029
- const { baseUrl, accessToken } = requireAuth();
23109
+ const { baseUrl, accessToken } = requireAuthToken();
23030
23110
  try {
23031
23111
  await deleteExchangeApiKey(baseUrl, accessToken, opts.name);
23032
23112
  if (opts.json) {
@@ -23041,7 +23121,7 @@ function createExchangeCommand() {
23041
23121
  }
23042
23122
  });
23043
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) => {
23044
- const { baseUrl, accessToken } = requireAuth();
23124
+ const { baseUrl, accessToken } = requireAuthToken();
23045
23125
  try {
23046
23126
  const result = await revalidateExchangeApiKey(baseUrl, accessToken, opts.name);
23047
23127
  if (opts.json) {
@@ -24540,6 +24620,39 @@ async function getLeaderboard(baseUrl, chainId, campaignAddress, rankBy, limit =
24540
24620
  // src/services/campaign-create.ts
24541
24621
  import { createHash as createHash2 } from "node:crypto";
24542
24622
 
24623
+ // src/lib/blockchain.ts
24624
+ function sleep2(ms) {
24625
+ return new Promise((resolve) => setTimeout(resolve, ms));
24626
+ }
24627
+ async function waitForConfirmations(provider, txHash, opts = {}) {
24628
+ const minConfirmations = opts.minConfirmations ?? 1;
24629
+ const pollMs = opts.pollMs ?? 3000;
24630
+ const timeoutMs = opts.timeoutMs ?? 120000;
24631
+ const started = Date.now();
24632
+ while (Date.now() - started <= timeoutMs) {
24633
+ const receipt = await provider.getTransactionReceipt(txHash);
24634
+ if (!receipt) {
24635
+ await sleep2(pollMs);
24636
+ continue;
24637
+ }
24638
+ let confirmations = 0;
24639
+ if (typeof receipt.confirmations === "function") {
24640
+ confirmations = await receipt.confirmations();
24641
+ } else {
24642
+ confirmations = receipt.confirmations ?? 0;
24643
+ }
24644
+ opts.onProgress?.(confirmations);
24645
+ if (confirmations >= minConfirmations) {
24646
+ return receipt;
24647
+ }
24648
+ await sleep2(pollMs);
24649
+ }
24650
+ throw new Error(`Timed out waiting for transaction confirmation: ${txHash}`);
24651
+ }
24652
+ function estimateGasWithBuffer(estimated, bps = 1200) {
24653
+ return estimated * BigInt(1e4 + bps) / BigInt(1e4);
24654
+ }
24655
+
24543
24656
  // src/lib/contracts.ts
24544
24657
  var HUMAN_PROTOCOL_CONTRACTS = {
24545
24658
  137: {
@@ -24617,6 +24730,10 @@ function getFundTokenAddress(chainId, symbol) {
24617
24730
  }
24618
24731
 
24619
24732
  // src/services/campaign-create.ts
24733
+ var CHAIN_NATIVE_SYMBOL = {
24734
+ 137: "MATIC",
24735
+ 1: "ETH"
24736
+ };
24620
24737
  function getProvider2(chainId) {
24621
24738
  return new JsonRpcProvider(getRpc(chainId), chainId, { staticNetwork: true, batchMaxCount: 1 });
24622
24739
  }
@@ -24655,7 +24772,51 @@ function buildManifest(params) {
24655
24772
  function hashManifest(manifest) {
24656
24773
  return createHash2("sha1").update(manifest).digest("hex");
24657
24774
  }
24658
- async function createCampaign(privateKey, chainId, params) {
24775
+ async function preflightCampaign(privateKey, chainId, params) {
24776
+ const contracts = getContracts(chainId);
24777
+ const provider = getProvider2(chainId);
24778
+ const wallet = new Wallet(privateKey, provider);
24779
+ const tokenAddress = getFundTokenAddress(chainId, params.fundToken);
24780
+ const fundTokenContract = new Contract(tokenAddress, ERC20_ABI, wallet);
24781
+ const [decimals, balance, allowance, nativeBalance, gasPrice] = await Promise.all([
24782
+ fundTokenContract.getFunction("decimals")(),
24783
+ fundTokenContract.getFunction("balanceOf")(wallet.address),
24784
+ fundTokenContract.getFunction("allowance")(wallet.address, contracts.escrowFactory),
24785
+ provider.getBalance(wallet.address),
24786
+ provider.getFeeData().then((d) => d.gasPrice ?? 0n)
24787
+ ]);
24788
+ const fundAmountWei = parseUnits(params.fundAmount, decimals);
24789
+ const needsApproval = allowance < fundAmountWei;
24790
+ const manifest = buildManifest(params);
24791
+ const manifestHash = hashManifest(manifest);
24792
+ const factory = new Contract(contracts.escrowFactory, ESCROW_FACTORY_ABI, wallet);
24793
+ const [approveGasEstimate, createGasEstimate] = await Promise.all([
24794
+ needsApproval ? fundTokenContract.getFunction("approve").estimateGas(contracts.escrowFactory, fundAmountWei) : Promise.resolve(0n),
24795
+ factory.getFunction("createFundAndSetupEscrow").estimateGas(tokenAddress, fundAmountWei, "hufi-campaign-launcher", ORACLES.reputationOracle, ORACLES.recordingOracle, ORACLES.exchangeOracle, manifest, manifestHash)
24796
+ ]);
24797
+ return {
24798
+ needsApproval,
24799
+ fundTokenBalance: balance,
24800
+ fundTokenDecimals: decimals,
24801
+ fundTokenSymbol: params.fundToken.toUpperCase(),
24802
+ approveGasEstimate,
24803
+ createGasEstimate,
24804
+ nativeBalance,
24805
+ gasPrice
24806
+ };
24807
+ }
24808
+ function estimateTotalGasCost(result, chainId) {
24809
+ const totalGasUnits = (result.needsApproval ? result.approveGasEstimate : 0n) + result.createGasEstimate;
24810
+ const bufferedGasUnits = estimateGasWithBuffer(totalGasUnits);
24811
+ const totalGasWei = bufferedGasUnits * result.gasPrice;
24812
+ const nativeSymbol = CHAIN_NATIVE_SYMBOL[chainId] ?? "ETH";
24813
+ return {
24814
+ totalGasWei,
24815
+ nativeSymbol,
24816
+ insufficientNative: result.nativeBalance < totalGasWei
24817
+ };
24818
+ }
24819
+ async function createCampaign(privateKey, chainId, params, onConfirmationProgress) {
24659
24820
  const contracts = getContracts(chainId);
24660
24821
  const provider = getProvider2(chainId);
24661
24822
  const wallet = new Wallet(privateKey, provider);
@@ -24665,14 +24826,27 @@ async function createCampaign(privateKey, chainId, params) {
24665
24826
  const fundAmountWei = parseUnits(params.fundAmount, decimals);
24666
24827
  const allowance = await hmtContract.getFunction("allowance")(wallet.address, contracts.escrowFactory);
24667
24828
  if (allowance < fundAmountWei) {
24668
- const approveTx = await hmtContract.getFunction("approve")(contracts.escrowFactory, fundAmountWei);
24669
- await approveTx.wait();
24829
+ const approveEstimate = await hmtContract.getFunction("approve").estimateGas(contracts.escrowFactory, fundAmountWei);
24830
+ const approveTx = await hmtContract.getFunction("approve")(contracts.escrowFactory, fundAmountWei, { gasLimit: estimateGasWithBuffer(approveEstimate) });
24831
+ await waitForConfirmations(provider, approveTx.hash, { minConfirmations: 1 });
24670
24832
  }
24671
24833
  const manifest = buildManifest(params);
24672
24834
  const manifestHash = hashManifest(manifest);
24673
24835
  const factory = new Contract(contracts.escrowFactory, ESCROW_FACTORY_ABI, wallet);
24674
- const tx = await factory.getFunction("createFundAndSetupEscrow")(tokenAddress, fundAmountWei, "hufi-campaign-launcher", ORACLES.reputationOracle, ORACLES.recordingOracle, ORACLES.exchangeOracle, manifest, manifestHash);
24675
- const receipt = await tx.wait();
24836
+ const createEstimate = await factory.getFunction("createFundAndSetupEscrow").estimateGas(tokenAddress, fundAmountWei, "hufi-campaign-launcher", ORACLES.reputationOracle, ORACLES.recordingOracle, ORACLES.exchangeOracle, manifest, manifestHash);
24837
+ const tx = await factory.getFunction("createFundAndSetupEscrow")(tokenAddress, fundAmountWei, "hufi-campaign-launcher", ORACLES.reputationOracle, ORACLES.recordingOracle, ORACLES.exchangeOracle, manifest, manifestHash, { gasLimit: estimateGasWithBuffer(createEstimate) });
24838
+ const receipt = await waitForConfirmations(provider, tx.hash, {
24839
+ minConfirmations: 1,
24840
+ onProgress: (confirmations2) => {
24841
+ onConfirmationProgress?.(confirmations2);
24842
+ }
24843
+ });
24844
+ let confirmations = 1;
24845
+ if (typeof receipt.confirmations === "function") {
24846
+ confirmations = await receipt.confirmations();
24847
+ } else {
24848
+ confirmations = receipt.confirmations ?? 1;
24849
+ }
24676
24850
  const iface = factory.interface;
24677
24851
  let escrowAddress = "";
24678
24852
  for (const log of receipt.logs) {
@@ -24686,13 +24860,15 @@ async function createCampaign(privateKey, chainId, params) {
24686
24860
  }
24687
24861
  return {
24688
24862
  escrowAddress: escrowAddress || "unknown",
24689
- txHash: receipt.hash
24863
+ txHash: receipt.hash,
24864
+ status: "confirmed",
24865
+ confirmations
24690
24866
  };
24691
24867
  }
24692
24868
 
24693
24869
  // src/services/launcher/campaign.ts
24694
- async function listLauncherCampaigns(baseUrl, chainId, limit = 20, status = "active") {
24695
- const url = `${baseUrl}/campaigns?chain_id=${chainId}&status=${status}&limit=${limit}`;
24870
+ async function listLauncherCampaigns(baseUrl, chainId, limit = 20, status = "active", page = 1) {
24871
+ const url = `${baseUrl}/campaigns?chain_id=${chainId}&status=${status}&limit=${limit}&page=${page}`;
24696
24872
  return await requestJson(url);
24697
24873
  }
24698
24874
  async function getLauncherCampaign(baseUrl, chainId, campaignAddress) {
@@ -24773,18 +24949,38 @@ async function withdrawHMT(privateKey, chainId) {
24773
24949
  return receipt.hash;
24774
24950
  }
24775
24951
 
24952
+ // src/lib/watch.ts
24953
+ function sleep3(ms) {
24954
+ return new Promise((resolve) => setTimeout(resolve, ms));
24955
+ }
24956
+ async function runWatchLoop(fn, options = {}) {
24957
+ const intervalMs = options.intervalMs ?? 1e4;
24958
+ const shouldContinue = options.shouldContinue ?? (() => true);
24959
+ while (shouldContinue()) {
24960
+ await fn();
24961
+ if (!shouldContinue()) {
24962
+ break;
24963
+ }
24964
+ await sleep3(intervalMs);
24965
+ }
24966
+ }
24967
+
24776
24968
  // src/commands/campaign.ts
24777
- function requireAuth2() {
24778
- const config = loadConfig();
24779
- if (!config.accessToken || !config.address) {
24780
- printText("Not authenticated. Run: hufi auth login --private-key <key>");
24781
- process.exit(1);
24969
+ function formatCampaignTimestamp(value) {
24970
+ if (!value)
24971
+ return "-";
24972
+ return value.replace("T", " ").replace(/\.\d+Z$/, "").replace(/Z$/, "");
24973
+ }
24974
+ function formatTokenAmount(value, decimals, displayDecimals = 2) {
24975
+ const amount = new bignumber_default(value).dividedBy(new bignumber_default(10).pow(decimals));
24976
+ const rounded = amount.decimalPlaces(displayDecimals, bignumber_default.ROUND_HALF_UP);
24977
+ return rounded.toFormat().replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
24978
+ }
24979
+ function formatCampaignCreateProgress(confirmations) {
24980
+ if (confirmations <= 0) {
24981
+ return "Transaction submitted. Waiting for confirmations...";
24782
24982
  }
24783
- return {
24784
- baseUrl: config.recordingApiUrl.replace(/\/+$/, ""),
24785
- accessToken: config.accessToken,
24786
- address: config.address
24787
- };
24983
+ return `Confirmations: ${confirmations}`;
24788
24984
  }
24789
24985
  function getLauncherUrl() {
24790
24986
  const config = loadConfig();
@@ -24792,10 +24988,11 @@ function getLauncherUrl() {
24792
24988
  }
24793
24989
  function createCampaignCommand() {
24794
24990
  const campaign = new Command("campaign").description("Campaign management commands");
24795
- 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("-l, --limit <n>", "Max results", Number, 20).option("--json", "Output as JSON").action(async (opts) => {
24991
+ 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) => {
24796
24992
  const config = loadConfig();
24797
24993
  try {
24798
- const launcherResult = await listLauncherCampaigns(getLauncherUrl(), opts.chainId, opts.limit, opts.status);
24994
+ const pageSize = opts.pageSize ?? opts.limit;
24995
+ const launcherResult = await listLauncherCampaigns(getLauncherUrl(), opts.chainId, pageSize, opts.status, opts.page);
24799
24996
  let joinedKeys = new Set;
24800
24997
  if (config.accessToken) {
24801
24998
  const recordingUrl = config.recordingApiUrl.replace(/\/+$/, "");
@@ -24821,10 +25018,6 @@ function createCampaignCommand() {
24821
25018
  const joined = joinedKeys.has(key);
24822
25019
  const tag = joined ? " [JOINED]" : "";
24823
25020
  const decimals = c.fund_token_decimals ?? 0;
24824
- const fmt = (v) => {
24825
- const bn = new bignumber_default(v).dividedBy(new bignumber_default(10).pow(decimals));
24826
- return bn.toFormat();
24827
- };
24828
25021
  const fundAmount = new bignumber_default(c.fund_amount);
24829
25022
  const balanceNum = new bignumber_default(c.balance);
24830
25023
  const pct = fundAmount.gt(0) ? balanceNum.dividedBy(fundAmount).times(100).toFixed(1) : "0.0";
@@ -24832,13 +25025,16 @@ function createCampaignCommand() {
24832
25025
  printText(` chain: ${c.chain_id}`);
24833
25026
  printText(` address: ${c.address}`);
24834
25027
  printText(` status: ${c.status}`);
24835
- printText(` duration: ${c.start_date?.split("T")[0] ?? "-"} ~ ${c.end_date?.split("T")[0] ?? "-"}`);
24836
- printText(` funded: ${fmt(c.fund_amount)} ${c.fund_token_symbol} paid: ${fmt(c.amount_paid)} balance: ${fmt(c.balance)} (${pct}%)`);
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}%)`);
24837
25030
  printText("");
24838
25031
  }
24839
25032
  if (opts.status === "active") {
24840
25033
  printText("Tip: use --status completed, --status cancelled, or --status to_cancel to see other campaigns.");
24841
25034
  }
25035
+ if (launcherResult.has_more) {
25036
+ printText(`Tip: more campaigns available, try --page ${opts.page + 1} --page-size ${pageSize}.`);
25037
+ }
24842
25038
  }
24843
25039
  }
24844
25040
  } catch (err) {
@@ -24858,12 +25054,11 @@ function createCampaignCommand() {
24858
25054
  printText(` chain: ${c.chain_id}`);
24859
25055
  printText(` status: ${c.status}`);
24860
25056
  const showDecimals = c.fund_token_decimals ?? 0;
24861
- const showFmt = (v) => new bignumber_default(v).dividedBy(new bignumber_default(10).pow(showDecimals)).toFormat();
24862
- printText(` funded: ${showFmt(c.fund_amount)} ${c.fund_token_symbol}`);
24863
- printText(` balance: ${showFmt(c.balance)} ${c.fund_token_symbol}`);
24864
- printText(` paid: ${showFmt(c.amount_paid)} ${c.fund_token_symbol}`);
24865
- printText(` start: ${c.start_date}`);
24866
- printText(` end: ${c.end_date}`);
25057
+ printText(` funded: ${formatTokenAmount(c.fund_amount, showDecimals)} ${c.fund_token_symbol}`);
25058
+ printText(` balance: ${formatTokenAmount(c.balance, showDecimals)} ${c.fund_token_symbol}`);
25059
+ printText(` paid: ${formatTokenAmount(c.amount_paid, showDecimals)} ${c.fund_token_symbol}`);
25060
+ printText(` start: ${formatCampaignTimestamp(c.start_date)}`);
25061
+ printText(` end: ${formatCampaignTimestamp(c.end_date)}`);
24867
25062
  printText(` launcher: ${c.launcher}`);
24868
25063
  }
24869
25064
  } catch (err) {
@@ -24873,7 +25068,7 @@ function createCampaignCommand() {
24873
25068
  }
24874
25069
  });
24875
25070
  campaign.command("joined").description("List campaigns you have joined").option("-l, --limit <n>", "Max results", Number, 20).option("--json", "Output as JSON").action(async (opts) => {
24876
- const { baseUrl, accessToken } = requireAuth2();
25071
+ const { baseUrl, accessToken } = requireAuthAddress();
24877
25072
  try {
24878
25073
  const result = await listJoinedCampaigns(baseUrl, accessToken, opts.limit);
24879
25074
  if (opts.json) {
@@ -24901,7 +25096,7 @@ function createCampaignCommand() {
24901
25096
  statusCmd.help();
24902
25097
  return;
24903
25098
  }
24904
- const { baseUrl, accessToken } = requireAuth2();
25099
+ const { baseUrl, accessToken } = requireAuthAddress();
24905
25100
  try {
24906
25101
  const status = await checkJoinStatus(baseUrl, accessToken, opts.chainId, opts.address);
24907
25102
  if (opts.json) {
@@ -24924,7 +25119,7 @@ function createCampaignCommand() {
24924
25119
  joinCmd.help();
24925
25120
  return;
24926
25121
  }
24927
- const { baseUrl, accessToken } = requireAuth2();
25122
+ const { baseUrl, accessToken } = requireAuthAddress();
24928
25123
  try {
24929
25124
  const joinStatus = await checkJoinStatus(baseUrl, accessToken, opts.chainId, opts.address);
24930
25125
  if (joinStatus.status === "already_joined") {
@@ -24949,25 +25144,53 @@ function createCampaignCommand() {
24949
25144
  process.exitCode = 1;
24950
25145
  }
24951
25146
  });
24952
- const progressCmd = campaign.command("progress").description("Check your progress in a campaign").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("-a, --address <address>", "Campaign escrow address").option("--json", "Output as JSON").action(async (opts) => {
25147
+ const progressCmd = campaign.command("progress").description("Check your progress in a campaign").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("-a, --address <address>", "Campaign escrow address").option("--watch", "Poll continuously").option("--interval <ms>", "Polling interval in ms", Number, 1e4).option("--json", "Output as JSON").action(async (opts) => {
24953
25148
  if (!opts.address) {
24954
25149
  progressCmd.help();
24955
25150
  return;
24956
25151
  }
24957
- const { baseUrl, accessToken } = requireAuth2();
25152
+ const { baseUrl, accessToken } = requireAuthAddress();
24958
25153
  try {
24959
- const result = await getMyProgress(baseUrl, accessToken, opts.chainId, opts.address);
24960
- if (opts.json) {
24961
- printJson(result);
24962
- } else {
24963
- const r = result;
24964
- if (r.message) {
24965
- printText(String(r.message));
25154
+ let running = true;
25155
+ let hasRunOnce = false;
25156
+ let watchStoppedBySignal = false;
25157
+ const stop = () => {
25158
+ running = false;
25159
+ watchStoppedBySignal = true;
25160
+ printText("Stopped watching progress.");
25161
+ };
25162
+ if (opts.watch) {
25163
+ process.once("SIGINT", stop);
25164
+ }
25165
+ await runWatchLoop(async () => {
25166
+ hasRunOnce = true;
25167
+ const result = await getMyProgress(baseUrl, accessToken, opts.chainId, opts.address);
25168
+ if (opts.json) {
25169
+ printJson(result);
24966
25170
  } else {
24967
- for (const [key, value] of Object.entries(r)) {
24968
- printText(` ${key}: ${value}`);
25171
+ const r = result;
25172
+ if (r.message) {
25173
+ printText(String(r.message));
25174
+ } else {
25175
+ for (const [key, value] of Object.entries(r)) {
25176
+ printText(` ${key}: ${value}`);
25177
+ }
24969
25178
  }
24970
25179
  }
25180
+ if (opts.watch) {
25181
+ printText("---");
25182
+ }
25183
+ }, {
25184
+ intervalMs: opts.interval,
25185
+ shouldContinue: () => opts.watch ? running : !hasRunOnce
25186
+ });
25187
+ if (opts.watch) {
25188
+ process.removeListener("SIGINT", stop);
25189
+ if (watchStoppedBySignal) {
25190
+ process.exitCode = 0;
25191
+ }
25192
+ } else {
25193
+ return;
24971
25194
  }
24972
25195
  } catch (err) {
24973
25196
  const message = err instanceof Error ? err.message : String(err);
@@ -25053,16 +25276,6 @@ function createCampaignCommand() {
25053
25276
  printText("--minimum-balance-target is required for threshold campaigns.");
25054
25277
  process.exit(1);
25055
25278
  }
25056
- try {
25057
- const stakingInfo = await getStakingInfo(address, opts.chainId);
25058
- if (Number(stakingInfo.stakedTokens) <= 0) {
25059
- printText("You must stake HMT before creating a campaign.");
25060
- printText("Run: hufi staking stake -a <amount>");
25061
- process.exit(1);
25062
- }
25063
- } catch {
25064
- printText("Warning: could not verify staking status.");
25065
- }
25066
25279
  const params = {
25067
25280
  type: typeMap[type],
25068
25281
  exchange: opts.exchange,
@@ -25076,12 +25289,65 @@ function createCampaignCommand() {
25076
25289
  dailyBalanceTarget: opts.dailyBalanceTarget,
25077
25290
  minimumBalanceTarget: opts.minimumBalanceTarget
25078
25291
  };
25292
+ let minimumStake = "0";
25293
+ try {
25294
+ const stakingInfo = await getStakingInfo(address, opts.chainId);
25295
+ minimumStake = stakingInfo.minimumStake;
25296
+ if (Number(stakingInfo.stakedTokens) < Number(minimumStake)) {
25297
+ printText("Insufficient staked HMT to create a campaign.");
25298
+ printText(` Required: ${Number(minimumStake).toLocaleString()} HMT (minimum stake)`);
25299
+ printText(` Your stake: ${Number(stakingInfo.stakedTokens).toLocaleString()} HMT`);
25300
+ printText("");
25301
+ printText("Stake more HMT with: hufi staking stake -a <amount>");
25302
+ process.exit(1);
25303
+ }
25304
+ } catch (err) {
25305
+ const message = err instanceof Error ? err.message : String(err);
25306
+ printText(`Warning: could not verify staking status: ${message}`);
25307
+ printText("Proceeding anyway...");
25308
+ }
25309
+ let preflight;
25079
25310
  try {
25080
- printText(`Creating ${type} campaign on ${opts.exchange}...`);
25081
- printText(` Fund: ${opts.fundAmount} ${opts.fundToken}`);
25082
- printText(` Duration: ${opts.startDate} ~ ${opts.endDate}`);
25083
- printText("");
25084
- const result = await createCampaign(privateKey, opts.chainId, params);
25311
+ preflight = await preflightCampaign(privateKey, opts.chainId, params);
25312
+ } catch (err) {
25313
+ const message = err instanceof Error ? err.message : String(err);
25314
+ printText(`Preflight check failed: ${message}`);
25315
+ process.exitCode = 1;
25316
+ return;
25317
+ }
25318
+ const fundAmountWei = BigInt(Math.floor(Number(opts.fundAmount) * 10 ** preflight.fundTokenDecimals));
25319
+ if (preflight.fundTokenBalance < fundAmountWei) {
25320
+ printText(`Insufficient ${preflight.fundTokenSymbol} balance.`);
25321
+ printText(` Required: ${opts.fundAmount} ${preflight.fundTokenSymbol}`);
25322
+ printText(` Balance: ${formatUnits(preflight.fundTokenBalance, preflight.fundTokenDecimals)} ${preflight.fundTokenSymbol}`);
25323
+ process.exit(1);
25324
+ }
25325
+ const gasCost = estimateTotalGasCost(preflight, opts.chainId);
25326
+ printText("Campaign creation summary:");
25327
+ printText(` Type: ${type} on ${opts.exchange}`);
25328
+ printText(` Symbol: ${opts.symbol}`);
25329
+ printText(` Fund: ${opts.fundAmount} ${preflight.fundTokenSymbol}`);
25330
+ printText(` Duration: ${opts.startDate} ~ ${opts.endDate}`);
25331
+ printText(` Chain: ${opts.chainId}`);
25332
+ printText("");
25333
+ printText("Estimated gas costs:");
25334
+ if (preflight.needsApproval) {
25335
+ printText(` Approval: ~${preflight.approveGasEstimate.toLocaleString()} gas`);
25336
+ }
25337
+ printText(` Creation: ~${preflight.createGasEstimate.toLocaleString()} gas`);
25338
+ printText(` Total: ~${formatUnits(gasCost.totalGasWei, 18)} ${gasCost.nativeSymbol}`);
25339
+ printText("");
25340
+ if (gasCost.insufficientNative) {
25341
+ printText(`Insufficient ${gasCost.nativeSymbol} for gas.`);
25342
+ printText(` Balance: ${formatUnits(preflight.nativeBalance, 18)} ${gasCost.nativeSymbol}`);
25343
+ printText(` Needed: ~${formatUnits(gasCost.totalGasWei, 18)} ${gasCost.nativeSymbol}`);
25344
+ process.exit(1);
25345
+ }
25346
+ try {
25347
+ printText(formatCampaignCreateProgress(0));
25348
+ const result = await createCampaign(privateKey, opts.chainId, params, (confirmations) => {
25349
+ printText(formatCampaignCreateProgress(confirmations));
25350
+ });
25085
25351
  if (opts.json) {
25086
25352
  printJson(result);
25087
25353
  } else {
@@ -25214,22 +25480,34 @@ Send HMT to this address on Polygon (chain 137) or Ethereum (chain 1).`);
25214
25480
  return staking;
25215
25481
  }
25216
25482
 
25217
- // src/commands/dashboard.ts
25218
- function requireAuth3() {
25219
- const config = loadConfig();
25220
- if (!config.accessToken || !config.address) {
25221
- printText("Not authenticated. Run: hufi auth login --private-key <key>");
25222
- process.exit(1);
25483
+ // src/lib/export.ts
25484
+ function toCsvRows(rows) {
25485
+ if (rows.length === 0) {
25486
+ return "";
25223
25487
  }
25224
- return {
25225
- baseUrl: config.recordingApiUrl.replace(/\/+$/, ""),
25226
- accessToken: config.accessToken,
25227
- address: config.address
25228
- };
25488
+ const first = rows[0];
25489
+ if (!first) {
25490
+ return "";
25491
+ }
25492
+ const headers = Object.keys(first);
25493
+ const headerLine = headers.join(",");
25494
+ const lines = rows.map((row) => headers.map((key) => {
25495
+ const value = row[key];
25496
+ const raw = value === null || value === undefined ? "" : String(value);
25497
+ if (raw.includes(",") || raw.includes(`
25498
+ `) || raw.includes('"')) {
25499
+ return `"${raw.replaceAll('"', '""')}"`;
25500
+ }
25501
+ return raw;
25502
+ }).join(","));
25503
+ return [headerLine, ...lines].join(`
25504
+ `);
25229
25505
  }
25506
+
25507
+ // src/commands/dashboard.ts
25230
25508
  function createDashboardCommand() {
25231
- const dashboard = new Command("dashboard").description("Portfolio overview — staking, campaigns, and earnings").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--json", "Output as JSON").action(async (opts) => {
25232
- const { baseUrl, accessToken, address } = requireAuth3();
25509
+ const dashboard = new Command("dashboard").description("Portfolio overview — staking, campaigns, and earnings").option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--export <format>", "Export format: csv|json").option("--json", "Output as JSON").action(async (opts) => {
25510
+ const { baseUrl, accessToken, address } = requireAuthAddress();
25233
25511
  try {
25234
25512
  const [stakingInfo, campaignsResult] = await Promise.all([
25235
25513
  getStakingInfo(address, opts.chainId).catch(() => null),
@@ -25254,13 +25532,39 @@ function createDashboardCommand() {
25254
25532
  }
25255
25533
  });
25256
25534
  const progressResults = await Promise.all(progressPromises);
25535
+ const summary = {
25536
+ address,
25537
+ chainId: opts.chainId,
25538
+ staking: stakingInfo,
25539
+ activeCampaigns: progressResults
25540
+ };
25541
+ if (opts.export) {
25542
+ const format = String(opts.export).toLowerCase();
25543
+ if (format === "json") {
25544
+ printJson(summary);
25545
+ return;
25546
+ }
25547
+ if (format === "csv") {
25548
+ const rows = progressResults.map(({ campaign, progress }) => {
25549
+ const r = campaign;
25550
+ const p = progress ?? {};
25551
+ return {
25552
+ exchange: String(r.exchange_name ?? ""),
25553
+ symbol: String(r.symbol ?? ""),
25554
+ type: String(r.type ?? ""),
25555
+ campaign_address: String(r.address ?? r.escrow_address ?? ""),
25556
+ my_score: String(p.my_score ?? "")
25557
+ };
25558
+ });
25559
+ printText(toCsvRows(rows));
25560
+ return;
25561
+ }
25562
+ printText("Invalid export format. Use csv or json.");
25563
+ process.exitCode = 1;
25564
+ return;
25565
+ }
25257
25566
  if (opts.json) {
25258
- printJson({
25259
- address,
25260
- chainId: opts.chainId,
25261
- staking: stakingInfo,
25262
- activeCampaigns: progressResults
25263
- });
25567
+ printJson(summary);
25264
25568
  return;
25265
25569
  }
25266
25570
  printText(`Wallet: ${address} Chain: ${opts.chainId}
@@ -25296,7 +25600,7 @@ function createDashboardCommand() {
25296
25600
 
25297
25601
  // src/cli.ts
25298
25602
  var program2 = new Command;
25299
- program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("0.8.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) => {
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) => {
25300
25604
  const opts = thisCommand.opts();
25301
25605
  if (opts.configFile) {
25302
25606
  setConfigFile(opts.configFile);
@@ -25304,6 +25608,15 @@ program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("0
25304
25608
  if (opts.keyFile) {
25305
25609
  setKeyFile(opts.keyFile);
25306
25610
  }
25611
+ const validation = validateConfig(loadConfig());
25612
+ if (!validation.valid) {
25613
+ console.error("Invalid configuration:");
25614
+ for (const issue of validation.issues) {
25615
+ console.error(`- ${issue}`);
25616
+ }
25617
+ console.error("Fix config in ~/.hufi-cli/config.json or pass --config-file.");
25618
+ process.exit(1);
25619
+ }
25307
25620
  });
25308
25621
  program2.addCommand(createAuthCommand());
25309
25622
  program2.addCommand(createExchangeCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hufi-cli",
3
- "version": "0.8.1",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hufi": "./dist/cli.js"