hufi-cli 0.8.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +402 -94
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,8 @@ 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
 
@@ -141,6 +143,8 @@ Portfolio overview — staking, active campaigns, and progress in one view.
141
143
  ```bash
142
144
  hufi dashboard # full overview
143
145
  hufi dashboard --json # machine output
146
+ hufi dashboard --export csv # export active campaign rows as CSV
147
+ hufi dashboard --export json
144
148
  ```
145
149
 
146
150
  ## 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,28 @@ 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 formatCampaignCreateProgress(confirmations) {
24970
+ if (confirmations <= 0) {
24971
+ return "Transaction submitted. Waiting for confirmations...";
24782
24972
  }
24783
- return {
24784
- baseUrl: config.recordingApiUrl.replace(/\/+$/, ""),
24785
- accessToken: config.accessToken,
24786
- address: config.address
24787
- };
24973
+ return `Confirmations: ${confirmations}`;
24788
24974
  }
24789
24975
  function getLauncherUrl() {
24790
24976
  const config = loadConfig();
@@ -24792,10 +24978,11 @@ function getLauncherUrl() {
24792
24978
  }
24793
24979
  function createCampaignCommand() {
24794
24980
  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) => {
24981
+ 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
24982
  const config = loadConfig();
24797
24983
  try {
24798
- const launcherResult = await listLauncherCampaigns(getLauncherUrl(), opts.chainId, opts.limit, opts.status);
24984
+ const pageSize = opts.pageSize ?? opts.limit;
24985
+ const launcherResult = await listLauncherCampaigns(getLauncherUrl(), opts.chainId, pageSize, opts.status, opts.page);
24799
24986
  let joinedKeys = new Set;
24800
24987
  if (config.accessToken) {
24801
24988
  const recordingUrl = config.recordingApiUrl.replace(/\/+$/, "");
@@ -24839,6 +25026,9 @@ function createCampaignCommand() {
24839
25026
  if (opts.status === "active") {
24840
25027
  printText("Tip: use --status completed, --status cancelled, or --status to_cancel to see other campaigns.");
24841
25028
  }
25029
+ if (launcherResult.has_more) {
25030
+ printText(`Tip: more campaigns available, try --page ${opts.page + 1} --page-size ${pageSize}.`);
25031
+ }
24842
25032
  }
24843
25033
  }
24844
25034
  } catch (err) {
@@ -24873,7 +25063,7 @@ function createCampaignCommand() {
24873
25063
  }
24874
25064
  });
24875
25065
  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();
25066
+ const { baseUrl, accessToken } = requireAuthAddress();
24877
25067
  try {
24878
25068
  const result = await listJoinedCampaigns(baseUrl, accessToken, opts.limit);
24879
25069
  if (opts.json) {
@@ -24901,7 +25091,7 @@ function createCampaignCommand() {
24901
25091
  statusCmd.help();
24902
25092
  return;
24903
25093
  }
24904
- const { baseUrl, accessToken } = requireAuth2();
25094
+ const { baseUrl, accessToken } = requireAuthAddress();
24905
25095
  try {
24906
25096
  const status = await checkJoinStatus(baseUrl, accessToken, opts.chainId, opts.address);
24907
25097
  if (opts.json) {
@@ -24924,7 +25114,7 @@ function createCampaignCommand() {
24924
25114
  joinCmd.help();
24925
25115
  return;
24926
25116
  }
24927
- const { baseUrl, accessToken } = requireAuth2();
25117
+ const { baseUrl, accessToken } = requireAuthAddress();
24928
25118
  try {
24929
25119
  const joinStatus = await checkJoinStatus(baseUrl, accessToken, opts.chainId, opts.address);
24930
25120
  if (joinStatus.status === "already_joined") {
@@ -24949,25 +25139,53 @@ function createCampaignCommand() {
24949
25139
  process.exitCode = 1;
24950
25140
  }
24951
25141
  });
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) => {
25142
+ 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
25143
  if (!opts.address) {
24954
25144
  progressCmd.help();
24955
25145
  return;
24956
25146
  }
24957
- const { baseUrl, accessToken } = requireAuth2();
25147
+ const { baseUrl, accessToken } = requireAuthAddress();
24958
25148
  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));
25149
+ let running = true;
25150
+ let hasRunOnce = false;
25151
+ let watchStoppedBySignal = false;
25152
+ const stop = () => {
25153
+ running = false;
25154
+ watchStoppedBySignal = true;
25155
+ printText("Stopped watching progress.");
25156
+ };
25157
+ if (opts.watch) {
25158
+ process.once("SIGINT", stop);
25159
+ }
25160
+ await runWatchLoop(async () => {
25161
+ hasRunOnce = true;
25162
+ const result = await getMyProgress(baseUrl, accessToken, opts.chainId, opts.address);
25163
+ if (opts.json) {
25164
+ printJson(result);
24966
25165
  } else {
24967
- for (const [key, value] of Object.entries(r)) {
24968
- printText(` ${key}: ${value}`);
25166
+ const r = result;
25167
+ if (r.message) {
25168
+ printText(String(r.message));
25169
+ } else {
25170
+ for (const [key, value] of Object.entries(r)) {
25171
+ printText(` ${key}: ${value}`);
25172
+ }
24969
25173
  }
24970
25174
  }
25175
+ if (opts.watch) {
25176
+ printText("---");
25177
+ }
25178
+ }, {
25179
+ intervalMs: opts.interval,
25180
+ shouldContinue: () => opts.watch ? running : !hasRunOnce
25181
+ });
25182
+ if (opts.watch) {
25183
+ process.removeListener("SIGINT", stop);
25184
+ if (watchStoppedBySignal) {
25185
+ process.exitCode = 0;
25186
+ }
25187
+ } else {
25188
+ return;
24971
25189
  }
24972
25190
  } catch (err) {
24973
25191
  const message = err instanceof Error ? err.message : String(err);
@@ -25053,16 +25271,6 @@ function createCampaignCommand() {
25053
25271
  printText("--minimum-balance-target is required for threshold campaigns.");
25054
25272
  process.exit(1);
25055
25273
  }
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
25274
  const params = {
25067
25275
  type: typeMap[type],
25068
25276
  exchange: opts.exchange,
@@ -25076,12 +25284,65 @@ function createCampaignCommand() {
25076
25284
  dailyBalanceTarget: opts.dailyBalanceTarget,
25077
25285
  minimumBalanceTarget: opts.minimumBalanceTarget
25078
25286
  };
25287
+ let minimumStake = "0";
25288
+ try {
25289
+ const stakingInfo = await getStakingInfo(address, opts.chainId);
25290
+ minimumStake = stakingInfo.minimumStake;
25291
+ if (Number(stakingInfo.stakedTokens) < Number(minimumStake)) {
25292
+ printText("Insufficient staked HMT to create a campaign.");
25293
+ printText(` Required: ${Number(minimumStake).toLocaleString()} HMT (minimum stake)`);
25294
+ printText(` Your stake: ${Number(stakingInfo.stakedTokens).toLocaleString()} HMT`);
25295
+ printText("");
25296
+ printText("Stake more HMT with: hufi staking stake -a <amount>");
25297
+ process.exit(1);
25298
+ }
25299
+ } catch (err) {
25300
+ const message = err instanceof Error ? err.message : String(err);
25301
+ printText(`Warning: could not verify staking status: ${message}`);
25302
+ printText("Proceeding anyway...");
25303
+ }
25304
+ let preflight;
25079
25305
  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);
25306
+ preflight = await preflightCampaign(privateKey, opts.chainId, params);
25307
+ } catch (err) {
25308
+ const message = err instanceof Error ? err.message : String(err);
25309
+ printText(`Preflight check failed: ${message}`);
25310
+ process.exitCode = 1;
25311
+ return;
25312
+ }
25313
+ const fundAmountWei = BigInt(Math.floor(Number(opts.fundAmount) * 10 ** preflight.fundTokenDecimals));
25314
+ if (preflight.fundTokenBalance < fundAmountWei) {
25315
+ printText(`Insufficient ${preflight.fundTokenSymbol} balance.`);
25316
+ printText(` Required: ${opts.fundAmount} ${preflight.fundTokenSymbol}`);
25317
+ printText(` Balance: ${formatUnits(preflight.fundTokenBalance, preflight.fundTokenDecimals)} ${preflight.fundTokenSymbol}`);
25318
+ process.exit(1);
25319
+ }
25320
+ const gasCost = estimateTotalGasCost(preflight, opts.chainId);
25321
+ printText("Campaign creation summary:");
25322
+ printText(` Type: ${type} on ${opts.exchange}`);
25323
+ printText(` Symbol: ${opts.symbol}`);
25324
+ printText(` Fund: ${opts.fundAmount} ${preflight.fundTokenSymbol}`);
25325
+ printText(` Duration: ${opts.startDate} ~ ${opts.endDate}`);
25326
+ printText(` Chain: ${opts.chainId}`);
25327
+ printText("");
25328
+ printText("Estimated gas costs:");
25329
+ if (preflight.needsApproval) {
25330
+ printText(` Approval: ~${preflight.approveGasEstimate.toLocaleString()} gas`);
25331
+ }
25332
+ printText(` Creation: ~${preflight.createGasEstimate.toLocaleString()} gas`);
25333
+ printText(` Total: ~${formatUnits(gasCost.totalGasWei, 18)} ${gasCost.nativeSymbol}`);
25334
+ printText("");
25335
+ if (gasCost.insufficientNative) {
25336
+ printText(`Insufficient ${gasCost.nativeSymbol} for gas.`);
25337
+ printText(` Balance: ${formatUnits(preflight.nativeBalance, 18)} ${gasCost.nativeSymbol}`);
25338
+ printText(` Needed: ~${formatUnits(gasCost.totalGasWei, 18)} ${gasCost.nativeSymbol}`);
25339
+ process.exit(1);
25340
+ }
25341
+ try {
25342
+ printText(formatCampaignCreateProgress(0));
25343
+ const result = await createCampaign(privateKey, opts.chainId, params, (confirmations) => {
25344
+ printText(formatCampaignCreateProgress(confirmations));
25345
+ });
25085
25346
  if (opts.json) {
25086
25347
  printJson(result);
25087
25348
  } else {
@@ -25214,22 +25475,34 @@ Send HMT to this address on Polygon (chain 137) or Ethereum (chain 1).`);
25214
25475
  return staking;
25215
25476
  }
25216
25477
 
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);
25478
+ // src/lib/export.ts
25479
+ function toCsvRows(rows) {
25480
+ if (rows.length === 0) {
25481
+ return "";
25223
25482
  }
25224
- return {
25225
- baseUrl: config.recordingApiUrl.replace(/\/+$/, ""),
25226
- accessToken: config.accessToken,
25227
- address: config.address
25228
- };
25483
+ const first = rows[0];
25484
+ if (!first) {
25485
+ return "";
25486
+ }
25487
+ const headers = Object.keys(first);
25488
+ const headerLine = headers.join(",");
25489
+ const lines = rows.map((row) => headers.map((key) => {
25490
+ const value = row[key];
25491
+ const raw = value === null || value === undefined ? "" : String(value);
25492
+ if (raw.includes(",") || raw.includes(`
25493
+ `) || raw.includes('"')) {
25494
+ return `"${raw.replaceAll('"', '""')}"`;
25495
+ }
25496
+ return raw;
25497
+ }).join(","));
25498
+ return [headerLine, ...lines].join(`
25499
+ `);
25229
25500
  }
25501
+
25502
+ // src/commands/dashboard.ts
25230
25503
  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();
25504
+ 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) => {
25505
+ const { baseUrl, accessToken, address } = requireAuthAddress();
25233
25506
  try {
25234
25507
  const [stakingInfo, campaignsResult] = await Promise.all([
25235
25508
  getStakingInfo(address, opts.chainId).catch(() => null),
@@ -25254,13 +25527,39 @@ function createDashboardCommand() {
25254
25527
  }
25255
25528
  });
25256
25529
  const progressResults = await Promise.all(progressPromises);
25530
+ const summary = {
25531
+ address,
25532
+ chainId: opts.chainId,
25533
+ staking: stakingInfo,
25534
+ activeCampaigns: progressResults
25535
+ };
25536
+ if (opts.export) {
25537
+ const format = String(opts.export).toLowerCase();
25538
+ if (format === "json") {
25539
+ printJson(summary);
25540
+ return;
25541
+ }
25542
+ if (format === "csv") {
25543
+ const rows = progressResults.map(({ campaign, progress }) => {
25544
+ const r = campaign;
25545
+ const p = progress ?? {};
25546
+ return {
25547
+ exchange: String(r.exchange_name ?? ""),
25548
+ symbol: String(r.symbol ?? ""),
25549
+ type: String(r.type ?? ""),
25550
+ campaign_address: String(r.address ?? r.escrow_address ?? ""),
25551
+ my_score: String(p.my_score ?? "")
25552
+ };
25553
+ });
25554
+ printText(toCsvRows(rows));
25555
+ return;
25556
+ }
25557
+ printText("Invalid export format. Use csv or json.");
25558
+ process.exitCode = 1;
25559
+ return;
25560
+ }
25257
25561
  if (opts.json) {
25258
- printJson({
25259
- address,
25260
- chainId: opts.chainId,
25261
- staking: stakingInfo,
25262
- activeCampaigns: progressResults
25263
- });
25562
+ printJson(summary);
25264
25563
  return;
25265
25564
  }
25266
25565
  printText(`Wallet: ${address} Chain: ${opts.chainId}
@@ -25296,7 +25595,7 @@ function createDashboardCommand() {
25296
25595
 
25297
25596
  // src/cli.ts
25298
25597
  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) => {
25598
+ program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("1.0.0").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
25599
  const opts = thisCommand.opts();
25301
25600
  if (opts.configFile) {
25302
25601
  setConfigFile(opts.configFile);
@@ -25304,6 +25603,15 @@ program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("0
25304
25603
  if (opts.keyFile) {
25305
25604
  setKeyFile(opts.keyFile);
25306
25605
  }
25606
+ const validation = validateConfig(loadConfig());
25607
+ if (!validation.valid) {
25608
+ console.error("Invalid configuration:");
25609
+ for (const issue of validation.issues) {
25610
+ console.error(`- ${issue}`);
25611
+ }
25612
+ console.error("Fix config in ~/.hufi-cli/config.json or pass --config-file.");
25613
+ process.exit(1);
25614
+ }
25307
25615
  });
25308
25616
  program2.addCommand(createAuthCommand());
25309
25617
  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.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hufi": "./dist/cli.js"