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.
- package/README.md +4 -0
- package/dist/cli.js +402 -94
- 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
|
|
22702
|
-
|
|
22703
|
-
|
|
22704
|
-
|
|
22705
|
-
|
|
22706
|
-
|
|
22707
|
-
|
|
22708
|
-
|
|
22709
|
-
|
|
22710
|
-
|
|
22711
|
-
|
|
22712
|
-
|
|
22713
|
-
|
|
22714
|
-
|
|
22715
|
-
|
|
22716
|
-
|
|
22717
|
-
|
|
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/
|
|
22975
|
-
function
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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
|
|
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
|
|
24669
|
-
await
|
|
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
|
|
24675
|
-
const
|
|
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
|
|
24778
|
-
|
|
24779
|
-
|
|
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
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
25147
|
+
const { baseUrl, accessToken } = requireAuthAddress();
|
|
24958
25148
|
try {
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
|
|
24962
|
-
|
|
24963
|
-
|
|
24964
|
-
|
|
24965
|
-
|
|
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
|
-
|
|
24968
|
-
|
|
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
|
-
|
|
25081
|
-
|
|
25082
|
-
|
|
25083
|
-
printText(
|
|
25084
|
-
|
|
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/
|
|
25218
|
-
function
|
|
25219
|
-
|
|
25220
|
-
|
|
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
|
-
|
|
25225
|
-
|
|
25226
|
-
|
|
25227
|
-
|
|
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 } =
|
|
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.
|
|
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());
|