hufi-cli 0.6.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 +45 -0
  2. package/dist/cli.js +805 -161
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,6 +56,7 @@ hufi auth status
56
56
  | `campaign status` | Check join status |
57
57
  | `campaign progress` | Check your progress |
58
58
  | `campaign leaderboard` | View campaign leaderboard |
59
+ | `campaign create` | Create a new campaign (launch escrow on-chain) |
59
60
 
60
61
  ```bash
61
62
  hufi campaign list # list active campaigns
@@ -64,9 +65,38 @@ hufi campaign get --chain-id 137 --address 0x... # campaign details
64
65
  hufi campaign join --address 0x... # join (chain-id defaults to 137)
65
66
  hufi campaign status --address 0x... # check status
66
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
67
70
  hufi campaign leaderboard --address 0x... # leaderboard
68
71
  ```
69
72
 
73
+ #### Campaign Create
74
+
75
+ Requires staked HMT, gas, and fund tokens (USDT/USDC). Creates an escrow contract on-chain.
76
+
77
+ ```bash
78
+ # Market Making
79
+ hufi campaign create \
80
+ --type market_making --exchange mexc --symbol HMT/USDT \
81
+ --start-date 2026-04-01 --end-date 2026-05-01 \
82
+ --fund-token USDT --fund-amount 10000 \
83
+ --daily-volume-target 50000
84
+
85
+ # Holding
86
+ hufi campaign create \
87
+ --type holding --exchange mexc --symbol HMT \
88
+ --start-date 2026-04-01 --end-date 2026-05-01 \
89
+ --fund-token USDT --fund-amount 5000 \
90
+ --daily-balance-target 1000
91
+
92
+ # Threshold
93
+ hufi campaign create \
94
+ --type threshold --exchange mexc --symbol HMT \
95
+ --start-date 2026-04-01 --end-date 2026-05-01 \
96
+ --fund-token USDT --fund-amount 5000 \
97
+ --minimum-balance-target 500
98
+ ```
99
+
70
100
  Running `campaign status/join/progress/leaderboard` without `-a` shows help.
71
101
 
72
102
  ### exchange
@@ -75,10 +105,14 @@ Running `campaign status/join/progress/leaderboard` without `-a` shows help.
75
105
  |---------|-------------|
76
106
  | `exchange register` | Register a read-only exchange API key |
77
107
  | `exchange list` | List registered API keys |
108
+ | `exchange delete` | Delete API keys for an exchange |
109
+ | `exchange revalidate` | Revalidate an exchange API key |
78
110
 
79
111
  ```bash
80
112
  hufi exchange register --name mexc --api-key <key> --secret-key <secret>
81
113
  hufi exchange list
114
+ hufi exchange revalidate --name mexc
115
+ hufi exchange delete --name mexc
82
116
  ```
83
117
 
84
118
  ### staking
@@ -102,6 +136,17 @@ hufi staking withdraw # withdraw unlocked token
102
136
 
103
137
  Supports Polygon (chain 137) and Ethereum (chain 1). Staking contract: `0x01D1...07F1D` on Polygon.
104
138
 
139
+ ### dashboard
140
+
141
+ Portfolio overview — staking, active campaigns, and progress in one view.
142
+
143
+ ```bash
144
+ hufi dashboard # full overview
145
+ hufi dashboard --json # machine output
146
+ hufi dashboard --export csv # export active campaign rows as CSV
147
+ hufi dashboard --export json
148
+ ```
149
+
105
150
  ## Global Options
106
151
 
107
152
  | Option | Description |
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) {
@@ -22958,9 +23024,21 @@ async function listExchangeApiKeys(baseUrl, accessToken) {
22958
23024
  headers: authHeaders(accessToken)
22959
23025
  });
22960
23026
  }
23027
+ async function deleteExchangeApiKey(baseUrl, accessToken, exchangeName) {
23028
+ await requestJson(`${baseUrl}/exchange-api-keys/${exchangeName}`, {
23029
+ method: "DELETE",
23030
+ headers: authHeaders(accessToken)
23031
+ });
23032
+ }
23033
+ async function revalidateExchangeApiKey(baseUrl, accessToken, exchangeName) {
23034
+ return await requestJson(`${baseUrl}/exchange-api-keys/${exchangeName}/revalidate`, {
23035
+ method: "POST",
23036
+ headers: authHeaders(accessToken)
23037
+ });
23038
+ }
22961
23039
 
22962
- // src/commands/exchange.ts
22963
- function requireAuth() {
23040
+ // src/lib/require-auth.ts
23041
+ function requireAuthToken() {
22964
23042
  const config = loadConfig();
22965
23043
  if (!config.accessToken) {
22966
23044
  printText("Not authenticated. Run: hufi auth login --private-key <key>");
@@ -22971,10 +23049,24 @@ function requireAuth() {
22971
23049
  accessToken: config.accessToken
22972
23050
  };
22973
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
22974
23066
  function createExchangeCommand() {
22975
23067
  const exchange = new Command("exchange").description("Exchange API key management");
22976
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) => {
22977
- const { baseUrl, accessToken } = requireAuth();
23069
+ const { baseUrl, accessToken } = requireAuthToken();
22978
23070
  try {
22979
23071
  const result = await registerExchangeApiKey(baseUrl, accessToken, opts.name, opts.apiKey, opts.secretKey, opts.bitmartMemo);
22980
23072
  if (opts.json) {
@@ -22992,7 +23084,7 @@ function createExchangeCommand() {
22992
23084
  }
22993
23085
  });
22994
23086
  exchange.command("list").description("List registered exchange API keys").option("--json", "Output as JSON").action(async (opts) => {
22995
- const { baseUrl, accessToken } = requireAuth();
23087
+ const { baseUrl, accessToken } = requireAuthToken();
22996
23088
  try {
22997
23089
  const keys = await listExchangeApiKeys(baseUrl, accessToken);
22998
23090
  if (opts.json) {
@@ -23013,6 +23105,40 @@ function createExchangeCommand() {
23013
23105
  process.exitCode = 1;
23014
23106
  }
23015
23107
  });
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) => {
23109
+ const { baseUrl, accessToken } = requireAuthToken();
23110
+ try {
23111
+ await deleteExchangeApiKey(baseUrl, accessToken, opts.name);
23112
+ if (opts.json) {
23113
+ printJson({ deleted: true, exchange_name: opts.name });
23114
+ } else {
23115
+ printText(`Deleted API keys for ${opts.name}.`);
23116
+ }
23117
+ } catch (err) {
23118
+ const message = err instanceof Error ? err.message : String(err);
23119
+ printText(`Failed to delete exchange API keys: ${message}`);
23120
+ process.exitCode = 1;
23121
+ }
23122
+ });
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) => {
23124
+ const { baseUrl, accessToken } = requireAuthToken();
23125
+ try {
23126
+ const result = await revalidateExchangeApiKey(baseUrl, accessToken, opts.name);
23127
+ if (opts.json) {
23128
+ printJson(result);
23129
+ } 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(", ")}`);
23134
+ }
23135
+ }
23136
+ } catch (err) {
23137
+ const message = err instanceof Error ? err.message : String(err);
23138
+ printText(`Failed to revalidate exchange API key: ${message}`);
23139
+ process.exitCode = 1;
23140
+ }
23141
+ });
23016
23142
  return exchange;
23017
23143
  }
23018
23144
 
@@ -24491,9 +24617,258 @@ async function getLeaderboard(baseUrl, chainId, campaignAddress, rankBy, limit =
24491
24617
  return await requestJson(`${baseUrl}/campaigns/${chainId}-${campaignAddress}/leaderboard?rankBy=${rankBy}&limit=${limit}`);
24492
24618
  }
24493
24619
 
24620
+ // src/services/campaign-create.ts
24621
+ import { createHash as createHash2 } from "node:crypto";
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
+
24656
+ // src/lib/contracts.ts
24657
+ var HUMAN_PROTOCOL_CONTRACTS = {
24658
+ 137: {
24659
+ hmt: "0xc748B2A084F8eFc47E086ccdDD9b7e67aEb571BF",
24660
+ staking: "0x01D115E9E8bF0C58318793624CC662a030D07F1D",
24661
+ escrowFactory: "0x8D50dA7abe354a628a63ADfE23C19a2944612b83"
24662
+ },
24663
+ 1: {
24664
+ hmt: "0xd1ba9BAC957322D6e8c07a160a3A8dA11A0d2867",
24665
+ staking: "0x86Af9f6Cd34B69Db1B202223C6d6D109f2491569",
24666
+ escrowFactory: "0xE24e5C08E28331D24758b69A5E9f383D2bDD1c98"
24667
+ }
24668
+ };
24669
+ var RPC_URLS = {
24670
+ 137: [
24671
+ "https://polygon.drpc.org",
24672
+ "https://rpc.ankr.com/polygon",
24673
+ "https://polygon-mainnet.public.blastapi.io"
24674
+ ],
24675
+ 1: [
24676
+ "https://eth.drpc.org",
24677
+ "https://rpc.ankr.com/eth",
24678
+ "https://ethereum-rpc.publicnode.com"
24679
+ ]
24680
+ };
24681
+ var ORACLES = {
24682
+ exchangeOracle: "0x5b74d007ea08217bcde942a2132df43d568a6dca",
24683
+ recordingOracle: "0x3a2292c684e289fe5f07737b598fe0027ead5a0e",
24684
+ reputationOracle: "0x1519964f5cd2d9ef162b2b1b66f33669cca065c8"
24685
+ };
24686
+ var FUND_TOKENS = {
24687
+ 137: {
24688
+ USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
24689
+ USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"
24690
+ },
24691
+ 1: {
24692
+ USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
24693
+ USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
24694
+ }
24695
+ };
24696
+ var ESCROW_FACTORY_ABI = [
24697
+ "function createFundAndSetupEscrow(address token, uint256 amount, string jobRequesterId, address reputationOracle, address recordingOracle, address exchangeOracle, string manifest, string manifestHash) returns (address)",
24698
+ "event LaunchedV2(address indexed escrow, address indexed launcher, string jobRequesterId)"
24699
+ ];
24700
+ var ERC20_ABI = [
24701
+ "function balanceOf(address) external view returns (uint256)",
24702
+ "function allowance(address owner, address spender) external view returns (uint256)",
24703
+ "function approve(address spender, uint256 amount) external returns (bool)",
24704
+ "function decimals() external view returns (uint8)"
24705
+ ];
24706
+ function getContracts(chainId) {
24707
+ const c = HUMAN_PROTOCOL_CONTRACTS[chainId];
24708
+ if (!c) {
24709
+ throw new Error(`Unsupported chain ID: ${chainId}. Supported: ${Object.keys(HUMAN_PROTOCOL_CONTRACTS).join(", ")}`);
24710
+ }
24711
+ return c;
24712
+ }
24713
+ function getRpc(chainId) {
24714
+ const rpcs = RPC_URLS[chainId];
24715
+ if (!rpcs) {
24716
+ throw new Error(`No RPC URLs for chain ${chainId}`);
24717
+ }
24718
+ return rpcs[0];
24719
+ }
24720
+ function getFundTokenAddress(chainId, symbol) {
24721
+ const tokens = FUND_TOKENS[chainId];
24722
+ if (!tokens) {
24723
+ throw new Error(`No fund tokens for chain ${chainId}`);
24724
+ }
24725
+ const addr = tokens[symbol.toUpperCase()];
24726
+ if (!addr) {
24727
+ throw new Error(`Unknown fund token: ${symbol}. Supported: ${Object.keys(tokens).join(", ")}`);
24728
+ }
24729
+ return addr;
24730
+ }
24731
+
24732
+ // src/services/campaign-create.ts
24733
+ var CHAIN_NATIVE_SYMBOL = {
24734
+ 137: "MATIC",
24735
+ 1: "ETH"
24736
+ };
24737
+ function getProvider2(chainId) {
24738
+ return new JsonRpcProvider(getRpc(chainId), chainId, { staticNetwork: true, batchMaxCount: 1 });
24739
+ }
24740
+ function buildManifest(params) {
24741
+ const base = {
24742
+ exchange: params.exchange,
24743
+ start_date: params.startDate,
24744
+ end_date: params.endDate
24745
+ };
24746
+ switch (params.type) {
24747
+ case "MARKET_MAKING":
24748
+ return JSON.stringify({
24749
+ ...base,
24750
+ type: params.type,
24751
+ pair: params.pair,
24752
+ daily_volume_target: params.dailyVolumeTarget
24753
+ });
24754
+ case "HOLDING":
24755
+ return JSON.stringify({
24756
+ ...base,
24757
+ type: params.type,
24758
+ symbol: params.symbol,
24759
+ daily_balance_target: params.dailyBalanceTarget
24760
+ });
24761
+ case "THRESHOLD":
24762
+ return JSON.stringify({
24763
+ ...base,
24764
+ type: params.type,
24765
+ symbol: params.symbol,
24766
+ minimum_balance_target: params.minimumBalanceTarget
24767
+ });
24768
+ default:
24769
+ throw new Error(`Unknown campaign type: ${params.type}`);
24770
+ }
24771
+ }
24772
+ function hashManifest(manifest) {
24773
+ return createHash2("sha1").update(manifest).digest("hex");
24774
+ }
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) {
24820
+ const contracts = getContracts(chainId);
24821
+ const provider = getProvider2(chainId);
24822
+ const wallet = new Wallet(privateKey, provider);
24823
+ const tokenAddress = getFundTokenAddress(chainId, params.fundToken);
24824
+ const hmtContract = new Contract(tokenAddress, ERC20_ABI, wallet);
24825
+ const decimals = await hmtContract.getFunction("decimals")();
24826
+ const fundAmountWei = parseUnits(params.fundAmount, decimals);
24827
+ const allowance = await hmtContract.getFunction("allowance")(wallet.address, contracts.escrowFactory);
24828
+ if (allowance < fundAmountWei) {
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 });
24832
+ }
24833
+ const manifest = buildManifest(params);
24834
+ const manifestHash = hashManifest(manifest);
24835
+ const factory = new Contract(contracts.escrowFactory, ESCROW_FACTORY_ABI, wallet);
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
+ }
24850
+ const iface = factory.interface;
24851
+ let escrowAddress = "";
24852
+ for (const log of receipt.logs) {
24853
+ try {
24854
+ const parsed = iface.parseLog({ topics: log.topics, data: log.data });
24855
+ if (parsed?.name === "LaunchedV2") {
24856
+ escrowAddress = parsed.args.escrow;
24857
+ break;
24858
+ }
24859
+ } catch {}
24860
+ }
24861
+ return {
24862
+ escrowAddress: escrowAddress || "unknown",
24863
+ txHash: receipt.hash,
24864
+ status: "confirmed",
24865
+ confirmations
24866
+ };
24867
+ }
24868
+
24494
24869
  // src/services/launcher/campaign.ts
24495
- async function listLauncherCampaigns(baseUrl, chainId, limit = 20, status = "active") {
24496
- 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}`;
24497
24872
  return await requestJson(url);
24498
24873
  }
24499
24874
  async function getLauncherCampaign(baseUrl, chainId, campaignAddress) {
@@ -24501,29 +24876,113 @@ async function getLauncherCampaign(baseUrl, chainId, campaignAddress) {
24501
24876
  return await requestJson(url);
24502
24877
  }
24503
24878
 
24504
- // src/commands/campaign.ts
24505
- function requireAuth2() {
24506
- const config = loadConfig();
24507
- if (!config.accessToken || !config.address) {
24508
- printText("Not authenticated. Run: hufi auth login --private-key <key>");
24509
- process.exit(1);
24510
- }
24879
+ // src/services/staking.ts
24880
+ var STAKING_ABI = [
24881
+ "function getAvailableStake(address _staker) external view returns (uint256)",
24882
+ "function getStakedTokens(address _staker) external view returns (uint256)",
24883
+ "function minimumStake() external view returns (uint256)",
24884
+ "function lockPeriod() external view returns (uint32)",
24885
+ "function stake(uint256 _tokens) external",
24886
+ "function unstake(uint256 _tokens) external",
24887
+ "function withdraw() external"
24888
+ ];
24889
+ function getProvider3(chainId) {
24890
+ return new JsonRpcProvider(getRpc(chainId), chainId, { staticNetwork: true, batchMaxCount: 1 });
24891
+ }
24892
+ async function getStakingInfo(address, chainId) {
24893
+ const contracts = getContracts(chainId);
24894
+ const provider = getProvider3(chainId);
24895
+ const stakingContract = new Contract(contracts.staking, STAKING_ABI, provider);
24896
+ const hmtContract = new Contract(contracts.hmt, ERC20_ABI, provider);
24897
+ const [stakedTokens, availableStake, minimumStake, lockPeriod, hmtBalance] = await Promise.all([
24898
+ stakingContract.getFunction("getStakedTokens")(address),
24899
+ stakingContract.getFunction("getAvailableStake")(address),
24900
+ stakingContract.getFunction("minimumStake")(),
24901
+ stakingContract.getFunction("lockPeriod")(),
24902
+ hmtContract.getFunction("balanceOf")(address)
24903
+ ]);
24904
+ const lockedTokens = stakedTokens > availableStake ? stakedTokens - availableStake : 0n;
24511
24905
  return {
24512
- baseUrl: config.recordingApiUrl.replace(/\/+$/, ""),
24513
- accessToken: config.accessToken,
24514
- address: config.address
24906
+ stakedTokens: formatUnits(stakedTokens, 18),
24907
+ availableStake: formatUnits(availableStake, 18),
24908
+ lockedTokens: formatUnits(lockedTokens, 18),
24909
+ unlockBlock: 0,
24910
+ minimumStake: formatUnits(minimumStake, 18),
24911
+ lockPeriod: Number(lockPeriod),
24912
+ hmtBalance: formatUnits(hmtBalance, 18),
24913
+ chainId
24515
24914
  };
24516
24915
  }
24916
+ async function stakeHMT(privateKey, amount, chainId) {
24917
+ const contracts = getContracts(chainId);
24918
+ const provider = getProvider3(chainId);
24919
+ const wallet = new Wallet(privateKey, provider);
24920
+ const stakingContract = new Contract(contracts.staking, STAKING_ABI, wallet);
24921
+ const hmtContract = new Contract(contracts.hmt, ERC20_ABI, wallet);
24922
+ const amountWei = parseUnits(amount, 18);
24923
+ const allowance = await hmtContract.getFunction("allowance")(wallet.address, contracts.staking);
24924
+ if (allowance < amountWei) {
24925
+ const approveTx = await hmtContract.getFunction("approve")(contracts.staking, amountWei);
24926
+ await approveTx.wait();
24927
+ }
24928
+ const tx = await stakingContract.getFunction("stake")(amountWei);
24929
+ const receipt = await tx.wait();
24930
+ return receipt.hash;
24931
+ }
24932
+ async function unstakeHMT(privateKey, amount, chainId) {
24933
+ const contracts = getContracts(chainId);
24934
+ const provider = getProvider3(chainId);
24935
+ const wallet = new Wallet(privateKey, provider);
24936
+ const stakingContract = new Contract(contracts.staking, STAKING_ABI, wallet);
24937
+ const amountWei = parseUnits(amount, 18);
24938
+ const tx = await stakingContract.getFunction("unstake")(amountWei);
24939
+ const receipt = await tx.wait();
24940
+ return receipt.hash;
24941
+ }
24942
+ async function withdrawHMT(privateKey, chainId) {
24943
+ const contracts = getContracts(chainId);
24944
+ const provider = getProvider3(chainId);
24945
+ const wallet = new Wallet(privateKey, provider);
24946
+ const stakingContract = new Contract(contracts.staking, STAKING_ABI, wallet);
24947
+ const tx = await stakingContract.getFunction("withdraw")();
24948
+ const receipt = await tx.wait();
24949
+ return receipt.hash;
24950
+ }
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
+
24968
+ // src/commands/campaign.ts
24969
+ function formatCampaignCreateProgress(confirmations) {
24970
+ if (confirmations <= 0) {
24971
+ return "Transaction submitted. Waiting for confirmations...";
24972
+ }
24973
+ return `Confirmations: ${confirmations}`;
24974
+ }
24517
24975
  function getLauncherUrl() {
24518
24976
  const config = loadConfig();
24519
24977
  return (config.launcherApiUrl ?? "https://cl.hu.finance").replace(/\/+$/, "");
24520
24978
  }
24521
24979
  function createCampaignCommand() {
24522
24980
  const campaign = new Command("campaign").description("Campaign management commands");
24523
- 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) => {
24524
24982
  const config = loadConfig();
24525
24983
  try {
24526
- 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);
24527
24986
  let joinedKeys = new Set;
24528
24987
  if (config.accessToken) {
24529
24988
  const recordingUrl = config.recordingApiUrl.replace(/\/+$/, "");
@@ -24567,6 +25026,9 @@ function createCampaignCommand() {
24567
25026
  if (opts.status === "active") {
24568
25027
  printText("Tip: use --status completed, --status cancelled, or --status to_cancel to see other campaigns.");
24569
25028
  }
25029
+ if (launcherResult.has_more) {
25030
+ printText(`Tip: more campaigns available, try --page ${opts.page + 1} --page-size ${pageSize}.`);
25031
+ }
24570
25032
  }
24571
25033
  }
24572
25034
  } catch (err) {
@@ -24601,7 +25063,7 @@ function createCampaignCommand() {
24601
25063
  }
24602
25064
  });
24603
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) => {
24604
- const { baseUrl, accessToken } = requireAuth2();
25066
+ const { baseUrl, accessToken } = requireAuthAddress();
24605
25067
  try {
24606
25068
  const result = await listJoinedCampaigns(baseUrl, accessToken, opts.limit);
24607
25069
  if (opts.json) {
@@ -24629,7 +25091,7 @@ function createCampaignCommand() {
24629
25091
  statusCmd.help();
24630
25092
  return;
24631
25093
  }
24632
- const { baseUrl, accessToken } = requireAuth2();
25094
+ const { baseUrl, accessToken } = requireAuthAddress();
24633
25095
  try {
24634
25096
  const status = await checkJoinStatus(baseUrl, accessToken, opts.chainId, opts.address);
24635
25097
  if (opts.json) {
@@ -24652,7 +25114,7 @@ function createCampaignCommand() {
24652
25114
  joinCmd.help();
24653
25115
  return;
24654
25116
  }
24655
- const { baseUrl, accessToken } = requireAuth2();
25117
+ const { baseUrl, accessToken } = requireAuthAddress();
24656
25118
  try {
24657
25119
  const joinStatus = await checkJoinStatus(baseUrl, accessToken, opts.chainId, opts.address);
24658
25120
  if (joinStatus.status === "already_joined") {
@@ -24677,25 +25139,53 @@ function createCampaignCommand() {
24677
25139
  process.exitCode = 1;
24678
25140
  }
24679
25141
  });
24680
- 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) => {
24681
25143
  if (!opts.address) {
24682
25144
  progressCmd.help();
24683
25145
  return;
24684
25146
  }
24685
- const { baseUrl, accessToken } = requireAuth2();
25147
+ const { baseUrl, accessToken } = requireAuthAddress();
24686
25148
  try {
24687
- const result = await getMyProgress(baseUrl, accessToken, opts.chainId, opts.address);
24688
- if (opts.json) {
24689
- printJson(result);
24690
- } else {
24691
- const r = result;
24692
- if (r.message) {
24693
- 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);
24694
25165
  } else {
24695
- for (const [key, value] of Object.entries(r)) {
24696
- 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
+ }
24697
25173
  }
24698
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;
24699
25189
  }
24700
25190
  } catch (err) {
24701
25191
  const message = err instanceof Error ? err.message : String(err);
@@ -24731,120 +25221,146 @@ function createCampaignCommand() {
24731
25221
  process.exitCode = 1;
24732
25222
  }
24733
25223
  });
25224
+ const VALID_TYPES = ["market_making", "holding", "threshold"];
25225
+ campaign.command("create").description("Create a new campaign (launch escrow on-chain)").requiredOption("--type <type>", `Campaign type (${VALID_TYPES.join(", ")})`).requiredOption("--exchange <name>", "Exchange name (e.g. mexc, bybit)").requiredOption("--symbol <symbol>", "Token symbol or pair (e.g. HMT/USDT or HMT)").requiredOption("--start-date <date>", "Start date (YYYY-MM-DD)").requiredOption("--end-date <date>", "End date (YYYY-MM-DD)").requiredOption("--fund-token <token>", "Fund token (USDT or USDC)").requiredOption("--fund-amount <amount>", "Fund amount").option("--daily-volume-target <n>", "Daily volume target (market_making)", Number).option("--daily-balance-target <n>", "Daily balance target (holding)", Number).option("--minimum-balance-target <n>", "Minimum balance target (threshold)", Number).option("-c, --chain-id <id>", "Chain ID (default: from config)", Number, getDefaultChainId()).option("--json", "Output as JSON").action(async (opts) => {
25226
+ const privateKey = loadKey();
25227
+ if (!privateKey) {
25228
+ printText("No private key found. Run: hufi auth generate");
25229
+ process.exit(1);
25230
+ }
25231
+ const config = loadConfig();
25232
+ const address = config.address;
25233
+ if (!address) {
25234
+ printText("Not authenticated. Run: hufi auth login --private-key <key>");
25235
+ process.exit(1);
25236
+ }
25237
+ const type = opts.type.toLowerCase();
25238
+ if (!VALID_TYPES.includes(type)) {
25239
+ printText(`Invalid type: ${opts.type}. Must be one of: ${VALID_TYPES.join(", ")}`);
25240
+ process.exit(1);
25241
+ }
25242
+ const typeMap = {
25243
+ market_making: "MARKET_MAKING",
25244
+ holding: "HOLDING",
25245
+ threshold: "THRESHOLD"
25246
+ };
25247
+ const startDate = new Date(opts.startDate);
25248
+ const endDate = new Date(opts.endDate);
25249
+ const durationHours = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60);
25250
+ if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
25251
+ printText("Invalid date format. Use YYYY-MM-DD.");
25252
+ process.exit(1);
25253
+ }
25254
+ if (durationHours < 6) {
25255
+ printText("Campaign duration must be at least 6 hours.");
25256
+ process.exit(1);
25257
+ }
25258
+ if (durationHours > 100 * 24) {
25259
+ printText("Campaign duration must be at most 100 days.");
25260
+ process.exit(1);
25261
+ }
25262
+ if (type === "market_making" && !opts.dailyVolumeTarget) {
25263
+ printText("--daily-volume-target is required for market_making campaigns.");
25264
+ process.exit(1);
25265
+ }
25266
+ if (type === "holding" && !opts.dailyBalanceTarget) {
25267
+ printText("--daily-balance-target is required for holding campaigns.");
25268
+ process.exit(1);
25269
+ }
25270
+ if (type === "threshold" && !opts.minimumBalanceTarget) {
25271
+ printText("--minimum-balance-target is required for threshold campaigns.");
25272
+ process.exit(1);
25273
+ }
25274
+ const params = {
25275
+ type: typeMap[type],
25276
+ exchange: opts.exchange,
25277
+ pair: type === "market_making" ? opts.symbol : undefined,
25278
+ symbol: type !== "market_making" ? opts.symbol : undefined,
25279
+ startDate: startDate.toISOString(),
25280
+ endDate: endDate.toISOString(),
25281
+ fundToken: opts.fundToken.toUpperCase(),
25282
+ fundAmount: opts.fundAmount,
25283
+ dailyVolumeTarget: opts.dailyVolumeTarget,
25284
+ dailyBalanceTarget: opts.dailyBalanceTarget,
25285
+ minimumBalanceTarget: opts.minimumBalanceTarget
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;
25305
+ try {
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
+ });
25346
+ if (opts.json) {
25347
+ printJson(result);
25348
+ } else {
25349
+ printText(`Campaign created successfully!`);
25350
+ printText(` Escrow: ${result.escrowAddress}`);
25351
+ printText(` TX: ${result.txHash}`);
25352
+ }
25353
+ } catch (err) {
25354
+ const message = err instanceof Error ? err.message : String(err);
25355
+ printText(`Failed to create campaign: ${message}`);
25356
+ process.exitCode = 1;
25357
+ }
25358
+ });
24734
25359
  return campaign;
24735
25360
  }
24736
25361
 
24737
25362
  // src/commands/staking.ts
24738
25363
  var import_qrcode = __toESM(require_server(), 1);
24739
-
24740
- // src/services/staking.ts
24741
- var STAKING_CONTRACTS = {
24742
- 137: {
24743
- staking: "0x01D115E9E8bF0C58318793624CC662a030D07F1D",
24744
- hmt: "0xc748B2A084F8eFc47E086ccdDD9b7e67aEb571BF",
24745
- rpcs: [
24746
- "https://polygon.drpc.org",
24747
- "https://rpc.ankr.com/polygon",
24748
- "https://polygon-mainnet.public.blastapi.io"
24749
- ]
24750
- },
24751
- 1: {
24752
- staking: "0x86Af9f6Cd34B69Db1B202223C6d6D109f2491569",
24753
- hmt: "0xd1ba9BAC957322D6e8c07a160a3A8dA11A0d2867",
24754
- rpcs: [
24755
- "https://eth.drpc.org",
24756
- "https://rpc.ankr.com/eth",
24757
- "https://ethereum-rpc.publicnode.com"
24758
- ]
24759
- }
24760
- };
24761
- var STAKING_ABI = [
24762
- "function getAvailableStake(address _staker) external view returns (uint256)",
24763
- "function getStakedTokens(address _staker) external view returns (uint256)",
24764
- "function minimumStake() external view returns (uint256)",
24765
- "function lockPeriod() external view returns (uint32)",
24766
- "function stake(uint256 _tokens) external",
24767
- "function unstake(uint256 _tokens) external",
24768
- "function withdraw() external"
24769
- ];
24770
- var ERC20_ABI = [
24771
- "function balanceOf(address) external view returns (uint256)",
24772
- "function allowance(address owner, address spender) external view returns (uint256)",
24773
- "function approve(address spender, uint256 amount) external returns (bool)",
24774
- "function decimals() external view returns (uint8)"
24775
- ];
24776
- function getProvider2(chainId) {
24777
- const config = getConfig(chainId);
24778
- return new JsonRpcProvider(config.rpcs[0], chainId, { staticNetwork: true, batchMaxCount: 1 });
24779
- }
24780
- function getConfig(chainId) {
24781
- const config = STAKING_CONTRACTS[chainId];
24782
- if (!config) {
24783
- throw new Error(`Unsupported chain ID: ${chainId}. Supported: ${Object.keys(STAKING_CONTRACTS).join(", ")}`);
24784
- }
24785
- return config;
24786
- }
24787
- async function getStakingInfo(address, chainId) {
24788
- const config = getConfig(chainId);
24789
- const provider = getProvider2(chainId);
24790
- const stakingContract = new Contract(config.staking, STAKING_ABI, provider);
24791
- const hmtContract = new Contract(config.hmt, ERC20_ABI, provider);
24792
- const [stakedTokens, availableStake, minimumStake, lockPeriod, hmtBalance] = await Promise.all([
24793
- stakingContract.getFunction("getStakedTokens")(address),
24794
- stakingContract.getFunction("getAvailableStake")(address),
24795
- stakingContract.getFunction("minimumStake")(),
24796
- stakingContract.getFunction("lockPeriod")(),
24797
- hmtContract.getFunction("balanceOf")(address)
24798
- ]);
24799
- const lockedTokens = stakedTokens > availableStake ? stakedTokens - availableStake : 0n;
24800
- return {
24801
- stakedTokens: formatUnits(stakedTokens, 18),
24802
- availableStake: formatUnits(availableStake, 18),
24803
- lockedTokens: formatUnits(lockedTokens, 18),
24804
- unlockBlock: 0,
24805
- minimumStake: formatUnits(minimumStake, 18),
24806
- lockPeriod: Number(lockPeriod),
24807
- hmtBalance: formatUnits(hmtBalance, 18),
24808
- chainId
24809
- };
24810
- }
24811
- async function stakeHMT(privateKey, amount, chainId) {
24812
- const config = getConfig(chainId);
24813
- const provider = getProvider2(chainId);
24814
- const wallet = new Wallet(privateKey, provider);
24815
- const stakingContract = new Contract(config.staking, STAKING_ABI, wallet);
24816
- const hmtContract = new Contract(config.hmt, ERC20_ABI, wallet);
24817
- const amountWei = parseUnits(amount, 18);
24818
- const allowance = await hmtContract.getFunction("allowance")(wallet.address, config.staking);
24819
- if (allowance < amountWei) {
24820
- const approveTx = await hmtContract.getFunction("approve")(config.staking, amountWei);
24821
- await approveTx.wait();
24822
- }
24823
- const tx = await stakingContract.getFunction("stake")(amountWei);
24824
- const receipt = await tx.wait();
24825
- return receipt.hash;
24826
- }
24827
- async function unstakeHMT(privateKey, amount, chainId) {
24828
- const config = getConfig(chainId);
24829
- const provider = getProvider2(chainId);
24830
- const wallet = new Wallet(privateKey, provider);
24831
- const stakingContract = new Contract(config.staking, STAKING_ABI, wallet);
24832
- const amountWei = parseUnits(amount, 18);
24833
- const tx = await stakingContract.getFunction("unstake")(amountWei);
24834
- const receipt = await tx.wait();
24835
- return receipt.hash;
24836
- }
24837
- async function withdrawHMT(privateKey, chainId) {
24838
- const config = getConfig(chainId);
24839
- const provider = getProvider2(chainId);
24840
- const wallet = new Wallet(privateKey, provider);
24841
- const stakingContract = new Contract(config.staking, STAKING_ABI, wallet);
24842
- const tx = await stakingContract.getFunction("withdraw")();
24843
- const receipt = await tx.wait();
24844
- return receipt.hash;
24845
- }
24846
-
24847
- // src/commands/staking.ts
24848
25364
  function requireKey() {
24849
25365
  const key = loadKey();
24850
25366
  if (!key) {
@@ -24959,9 +25475,127 @@ Send HMT to this address on Polygon (chain 137) or Ethereum (chain 1).`);
24959
25475
  return staking;
24960
25476
  }
24961
25477
 
25478
+ // src/lib/export.ts
25479
+ function toCsvRows(rows) {
25480
+ if (rows.length === 0) {
25481
+ return "";
25482
+ }
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
+ `);
25500
+ }
25501
+
25502
+ // src/commands/dashboard.ts
25503
+ function createDashboardCommand() {
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();
25506
+ try {
25507
+ const [stakingInfo, campaignsResult] = await Promise.all([
25508
+ getStakingInfo(address, opts.chainId).catch(() => null),
25509
+ listJoinedCampaigns(baseUrl, accessToken, 50).catch(() => null)
25510
+ ]);
25511
+ const campaigns = campaignsResult?.results ?? [];
25512
+ const activeCampaigns = campaigns.filter((c) => {
25513
+ const r = c;
25514
+ return r.status === "active" || r.status === "ACTIVE";
25515
+ });
25516
+ const progressPromises = activeCampaigns.map(async (c) => {
25517
+ const r = c;
25518
+ const chainId = r.chain_id ?? opts.chainId;
25519
+ const campaignAddress = r.address ?? r.escrow_address ?? "";
25520
+ if (!campaignAddress)
25521
+ return { campaign: c, progress: null };
25522
+ try {
25523
+ const progress = await getMyProgress(baseUrl, accessToken, chainId, campaignAddress);
25524
+ return { campaign: c, progress };
25525
+ } catch {
25526
+ return { campaign: c, progress: null };
25527
+ }
25528
+ });
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
+ }
25561
+ if (opts.json) {
25562
+ printJson(summary);
25563
+ return;
25564
+ }
25565
+ printText(`Wallet: ${address} Chain: ${opts.chainId}
25566
+ `);
25567
+ if (stakingInfo) {
25568
+ printText("Staking");
25569
+ printText(` Staked: ${Number(stakingInfo.stakedTokens).toLocaleString()} HMT`);
25570
+ printText(` Available: ${Number(stakingInfo.availableStake).toLocaleString()} HMT`);
25571
+ printText(` Balance: ${Number(stakingInfo.hmtBalance).toLocaleString()} HMT`);
25572
+ printText("");
25573
+ }
25574
+ if (activeCampaigns.length === 0) {
25575
+ printText("No active campaigns.");
25576
+ } else {
25577
+ printText(`Active Campaigns (${activeCampaigns.length})`);
25578
+ for (const { campaign, progress } of progressResults) {
25579
+ const r = campaign;
25580
+ const exchange = r.exchange_name ?? "?";
25581
+ const symbol = r.symbol ?? "?";
25582
+ const type = r.type ?? "?";
25583
+ const score = progress && typeof progress === "object" && "my_score" in progress ? ` score: ${progress.my_score}` : "";
25584
+ printText(` ${exchange} ${symbol} (${type})${score}`);
25585
+ }
25586
+ }
25587
+ } catch (err) {
25588
+ const message = err instanceof Error ? err.message : String(err);
25589
+ printText(`Failed to load dashboard: ${message}`);
25590
+ process.exitCode = 1;
25591
+ }
25592
+ });
25593
+ return dashboard;
25594
+ }
25595
+
24962
25596
  // src/cli.ts
24963
25597
  var program2 = new Command;
24964
- program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("0.6.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) => {
24965
25599
  const opts = thisCommand.opts();
24966
25600
  if (opts.configFile) {
24967
25601
  setConfigFile(opts.configFile);
@@ -24969,9 +25603,19 @@ program2.name("hufi").description("CLI tool for hu.fi DeFi platform").version("0
24969
25603
  if (opts.keyFile) {
24970
25604
  setKeyFile(opts.keyFile);
24971
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
+ }
24972
25615
  });
24973
25616
  program2.addCommand(createAuthCommand());
24974
25617
  program2.addCommand(createExchangeCommand());
24975
25618
  program2.addCommand(createCampaignCommand());
24976
25619
  program2.addCommand(createStakingCommand());
25620
+ program2.addCommand(createDashboardCommand());
24977
25621
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hufi-cli",
3
- "version": "0.6.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hufi": "./dist/cli.js"