perp-cli 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/** Exported for smart landing page in index.ts */
|
|
3
|
+
export declare function getWalletSetupStatus(): {
|
|
4
|
+
hasWallets: boolean;
|
|
5
|
+
active: Record<string, string>;
|
|
6
|
+
wallets: Record<string, {
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
address: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
/** Exported so config.ts can resolve the active wallet key */
|
|
13
|
+
export declare function getActiveWalletKey(exchange: string): string | null;
|
|
14
|
+
export declare function registerWalletCommands(program: Command, isJson: () => boolean): void;
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { formatUsd, makeTable, printJson, jsonOk } from "../utils.js";
|
|
5
|
+
import { EXCHANGE_ENV_MAP, validateKey, loadEnvFile, setEnvVar, ENV_FILE } from "./init.js";
|
|
6
|
+
import { loadSettings, saveSettings } from "../settings.js";
|
|
7
|
+
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
8
|
+
const WALLETS_FILE = resolve(PERP_DIR, "wallets.json");
|
|
9
|
+
function ensurePerpDir() {
|
|
10
|
+
if (!existsSync(PERP_DIR))
|
|
11
|
+
mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
|
|
12
|
+
}
|
|
13
|
+
function loadStore() {
|
|
14
|
+
ensurePerpDir();
|
|
15
|
+
if (!existsSync(WALLETS_FILE))
|
|
16
|
+
return { wallets: {}, active: {} };
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(WALLETS_FILE, "utf-8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { wallets: {}, active: {} };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveStore(store) {
|
|
25
|
+
ensurePerpDir();
|
|
26
|
+
writeFileSync(WALLETS_FILE, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
27
|
+
}
|
|
28
|
+
/** Exported for smart landing page in index.ts */
|
|
29
|
+
export function getWalletSetupStatus() {
|
|
30
|
+
const store = loadStore();
|
|
31
|
+
const wallets = {};
|
|
32
|
+
for (const [k, v] of Object.entries(store.wallets)) {
|
|
33
|
+
wallets[k] = { name: v.name, type: v.type, address: v.address };
|
|
34
|
+
}
|
|
35
|
+
return { hasWallets: Object.keys(store.wallets).length > 0, active: store.active, wallets };
|
|
36
|
+
}
|
|
37
|
+
/** Exported so config.ts can resolve the active wallet key */
|
|
38
|
+
export function getActiveWalletKey(exchange) {
|
|
39
|
+
const store = loadStore();
|
|
40
|
+
const name = store.active[exchange];
|
|
41
|
+
if (!name)
|
|
42
|
+
return null;
|
|
43
|
+
return store.wallets[name]?.privateKey ?? null;
|
|
44
|
+
}
|
|
45
|
+
async function getSolanaBalances(address, isTestnet) {
|
|
46
|
+
const { Connection, PublicKey } = await import("@solana/web3.js");
|
|
47
|
+
const rpcUrl = isTestnet
|
|
48
|
+
? "https://api.devnet.solana.com"
|
|
49
|
+
: "https://api.mainnet-beta.solana.com";
|
|
50
|
+
const connection = new Connection(rpcUrl);
|
|
51
|
+
const pubkey = new PublicKey(address);
|
|
52
|
+
const balances = [];
|
|
53
|
+
const solLamports = await connection.getBalance(pubkey);
|
|
54
|
+
const solBalance = solLamports / 1e9;
|
|
55
|
+
balances.push({ token: "SOL", balance: solBalance.toFixed(6), usdValue: "" });
|
|
56
|
+
const usdcMint = isTestnet
|
|
57
|
+
? "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM"
|
|
58
|
+
: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
59
|
+
try {
|
|
60
|
+
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(pubkey, {
|
|
61
|
+
mint: new PublicKey(usdcMint),
|
|
62
|
+
});
|
|
63
|
+
let usdcBalance = 0;
|
|
64
|
+
for (const { account } of tokenAccounts.value) {
|
|
65
|
+
const parsed = account.data.parsed?.info;
|
|
66
|
+
if (parsed)
|
|
67
|
+
usdcBalance += Number(parsed.tokenAmount?.uiAmount ?? 0);
|
|
68
|
+
}
|
|
69
|
+
balances.push({
|
|
70
|
+
token: isTestnet ? "USDP" : "USDC",
|
|
71
|
+
balance: usdcBalance.toFixed(2),
|
|
72
|
+
usdValue: `$${formatUsd(usdcBalance)}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
balances.push({ token: isTestnet ? "USDP" : "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd");
|
|
80
|
+
const json = (await res.json());
|
|
81
|
+
balances[0].usdValue = `$${formatUsd(solBalance * (json.solana?.usd ?? 0))}`;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
balances[0].usdValue = "-";
|
|
85
|
+
}
|
|
86
|
+
return balances;
|
|
87
|
+
}
|
|
88
|
+
async function getEvmBalances(address, isTestnet) {
|
|
89
|
+
const { ethers } = await import("ethers");
|
|
90
|
+
const rpcUrl = isTestnet
|
|
91
|
+
? "https://sepolia-rollup.arbitrum.io/rpc"
|
|
92
|
+
: "https://arb1.arbitrum.io/rpc";
|
|
93
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
94
|
+
const balances = [];
|
|
95
|
+
const ethBal = await provider.getBalance(address);
|
|
96
|
+
const ethBalance = Number(ethers.formatEther(ethBal));
|
|
97
|
+
balances.push({ token: "ETH", balance: ethBalance.toFixed(6), usdValue: "" });
|
|
98
|
+
const usdcAddr = isTestnet
|
|
99
|
+
? "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
|
|
100
|
+
: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
|
|
101
|
+
try {
|
|
102
|
+
const usdc = new ethers.Contract(usdcAddr, ["function balanceOf(address) view returns (uint256)"], provider);
|
|
103
|
+
const rawBal = await usdc.balanceOf(address);
|
|
104
|
+
const usdcBalance = Number(ethers.formatUnits(rawBal, 6));
|
|
105
|
+
balances.push({ token: "USDC", balance: usdcBalance.toFixed(2), usdValue: `$${formatUsd(usdcBalance)}` });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
balances.push({ token: "USDC", balance: "0.00", usdValue: "$0.00" });
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd");
|
|
112
|
+
const json = (await res.json());
|
|
113
|
+
balances[0].usdValue = `$${formatUsd(ethBalance * (json.ethereum?.usd ?? 0))}`;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
balances[0].usdValue = "-";
|
|
117
|
+
}
|
|
118
|
+
return balances;
|
|
119
|
+
}
|
|
120
|
+
// ── Derive address from private key ──────────────────────────
|
|
121
|
+
async function deriveSolanaAddress(key) {
|
|
122
|
+
const { Keypair } = await import("@solana/web3.js");
|
|
123
|
+
const bs58 = (await import("bs58")).default;
|
|
124
|
+
try {
|
|
125
|
+
return Keypair.fromSecretKey(bs58.decode(key)).publicKey.toBase58();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
const arr = JSON.parse(key);
|
|
129
|
+
return Keypair.fromSecretKey(Uint8Array.from(arr)).publicKey.toBase58();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function deriveEvmAddress(key) {
|
|
133
|
+
const { ethers } = await import("ethers");
|
|
134
|
+
const pk = key.startsWith("0x") ? key : `0x${key}`;
|
|
135
|
+
return new ethers.Wallet(pk).address;
|
|
136
|
+
}
|
|
137
|
+
// ── Commands ──────────────────────────────────────────────────
|
|
138
|
+
export function registerWalletCommands(program, isJson) {
|
|
139
|
+
const wallet = program.command("wallet").description("Wallet management & on-chain balances");
|
|
140
|
+
// ── generate ──
|
|
141
|
+
const generate = wallet.command("generate").description("Generate a new wallet keypair");
|
|
142
|
+
generate
|
|
143
|
+
.command("solana")
|
|
144
|
+
.description("Generate a new Solana keypair")
|
|
145
|
+
.option("-n, --name <name>", "Wallet alias name", "default-sol")
|
|
146
|
+
.action(async (opts) => {
|
|
147
|
+
const { Keypair } = await import("@solana/web3.js");
|
|
148
|
+
const bs58 = (await import("bs58")).default;
|
|
149
|
+
const keypair = Keypair.generate();
|
|
150
|
+
const address = keypair.publicKey.toBase58();
|
|
151
|
+
const privkey = bs58.encode(keypair.secretKey);
|
|
152
|
+
const store = loadStore();
|
|
153
|
+
if (store.wallets[opts.name]) {
|
|
154
|
+
console.error(chalk.red(`\n Wallet "${opts.name}" already exists. Use a different name or 'wallet remove' first.\n`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
store.wallets[opts.name] = {
|
|
158
|
+
name: opts.name,
|
|
159
|
+
type: "solana",
|
|
160
|
+
address,
|
|
161
|
+
privateKey: privkey,
|
|
162
|
+
createdAt: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
if (!store.active.pacifica)
|
|
165
|
+
store.active.pacifica = opts.name;
|
|
166
|
+
saveStore(store);
|
|
167
|
+
// Also write to .env so loadPrivateKey finds it immediately
|
|
168
|
+
setEnvVar("PACIFICA_PRIVATE_KEY", privkey);
|
|
169
|
+
if (isJson())
|
|
170
|
+
return printJson(jsonOk({ name: opts.name, type: "solana", address }));
|
|
171
|
+
console.log(chalk.cyan.bold("\n New Solana Wallet\n"));
|
|
172
|
+
console.log(` Name: ${chalk.white.bold(opts.name)}`);
|
|
173
|
+
console.log(` Address: ${chalk.green(address)}`);
|
|
174
|
+
console.log(` Saved: ${chalk.gray("~/.perp/.env + wallets.json")}`);
|
|
175
|
+
if (store.active.pacifica === opts.name) {
|
|
176
|
+
console.log(chalk.cyan(`\n Active for: pacifica`));
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.red.bold("\n Fund this wallet before trading!\n"));
|
|
179
|
+
});
|
|
180
|
+
generate
|
|
181
|
+
.command("evm")
|
|
182
|
+
.description("Generate a new EVM wallet")
|
|
183
|
+
.option("-n, --name <name>", "Wallet alias name", "default-evm")
|
|
184
|
+
.action(async (opts) => {
|
|
185
|
+
const { ethers } = await import("ethers");
|
|
186
|
+
const w = ethers.Wallet.createRandom();
|
|
187
|
+
const store = loadStore();
|
|
188
|
+
if (store.wallets[opts.name]) {
|
|
189
|
+
console.error(chalk.red(`\n Wallet "${opts.name}" already exists. Use a different name or 'wallet remove' first.\n`));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
store.wallets[opts.name] = {
|
|
193
|
+
name: opts.name,
|
|
194
|
+
type: "evm",
|
|
195
|
+
address: w.address,
|
|
196
|
+
privateKey: w.privateKey,
|
|
197
|
+
createdAt: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
if (!store.active.hyperliquid)
|
|
200
|
+
store.active.hyperliquid = opts.name;
|
|
201
|
+
if (!store.active.lighter)
|
|
202
|
+
store.active.lighter = opts.name;
|
|
203
|
+
saveStore(store);
|
|
204
|
+
// Also write to .env so loadPrivateKey finds it immediately
|
|
205
|
+
setEnvVar("HL_PRIVATE_KEY", w.privateKey);
|
|
206
|
+
setEnvVar("LIGHTER_PRIVATE_KEY", w.privateKey);
|
|
207
|
+
if (isJson())
|
|
208
|
+
return printJson(jsonOk({ name: opts.name, type: "evm", address: w.address }));
|
|
209
|
+
console.log(chalk.cyan.bold("\n New EVM Wallet\n"));
|
|
210
|
+
console.log(` Name: ${chalk.white.bold(opts.name)}`);
|
|
211
|
+
console.log(` Address: ${chalk.green(w.address)}`);
|
|
212
|
+
console.log(` Saved: ${chalk.gray("~/.perp/.env + wallets.json")}`);
|
|
213
|
+
const activeFor = Object.entries(store.active)
|
|
214
|
+
.filter(([, v]) => v === opts.name)
|
|
215
|
+
.map(([k]) => k);
|
|
216
|
+
if (activeFor.length)
|
|
217
|
+
console.log(chalk.cyan(`\n Active for: ${activeFor.join(", ")}`));
|
|
218
|
+
console.log(chalk.red.bold("\n Fund this wallet before trading!\n"));
|
|
219
|
+
});
|
|
220
|
+
// ── import ──
|
|
221
|
+
const importCmd = wallet.command("import").description("Import an existing private key");
|
|
222
|
+
importCmd
|
|
223
|
+
.command("solana <privateKey>")
|
|
224
|
+
.description("Import a Solana private key (base58 or JSON array)")
|
|
225
|
+
.option("-n, --name <name>", "Wallet alias name", "imported-sol")
|
|
226
|
+
.action(async (privateKey, opts) => {
|
|
227
|
+
let address;
|
|
228
|
+
let normalizedKey = privateKey;
|
|
229
|
+
try {
|
|
230
|
+
const { Keypair } = await import("@solana/web3.js");
|
|
231
|
+
const bs58 = (await import("bs58")).default;
|
|
232
|
+
try {
|
|
233
|
+
const bytes = bs58.decode(privateKey);
|
|
234
|
+
const kp = Keypair.fromSecretKey(bytes);
|
|
235
|
+
address = kp.publicKey.toBase58();
|
|
236
|
+
normalizedKey = bs58.encode(kp.secretKey);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
const arr = JSON.parse(privateKey);
|
|
240
|
+
const kp = Keypair.fromSecretKey(Uint8Array.from(arr));
|
|
241
|
+
address = kp.publicKey.toBase58();
|
|
242
|
+
normalizedKey = bs58.encode(kp.secretKey);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
console.error(chalk.red("\n Invalid Solana private key.\n"));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const store = loadStore();
|
|
250
|
+
if (store.wallets[opts.name]) {
|
|
251
|
+
console.error(chalk.red(`\n Wallet "${opts.name}" already exists.\n`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
store.wallets[opts.name] = {
|
|
255
|
+
name: opts.name, type: "solana", address, privateKey: normalizedKey,
|
|
256
|
+
createdAt: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
if (!store.active.pacifica)
|
|
259
|
+
store.active.pacifica = opts.name;
|
|
260
|
+
saveStore(store);
|
|
261
|
+
// Also write to .env so loadPrivateKey finds it immediately
|
|
262
|
+
setEnvVar("PACIFICA_PRIVATE_KEY", normalizedKey);
|
|
263
|
+
if (isJson())
|
|
264
|
+
return printJson(jsonOk({ name: opts.name, type: "solana", address }));
|
|
265
|
+
console.log(chalk.cyan.bold("\n Solana Wallet Imported\n"));
|
|
266
|
+
console.log(` Name: ${chalk.white.bold(opts.name)}`);
|
|
267
|
+
console.log(` Address: ${chalk.green(address)}\n`);
|
|
268
|
+
});
|
|
269
|
+
importCmd
|
|
270
|
+
.command("evm <privateKey>")
|
|
271
|
+
.description("Import an EVM private key (0x hex)")
|
|
272
|
+
.option("-n, --name <name>", "Wallet alias name", "imported-evm")
|
|
273
|
+
.action(async (privateKey, opts) => {
|
|
274
|
+
const { ethers } = await import("ethers");
|
|
275
|
+
const pk = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
|
|
276
|
+
let address;
|
|
277
|
+
try {
|
|
278
|
+
address = new ethers.Wallet(pk).address;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
console.error(chalk.red("\n Invalid EVM private key.\n"));
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
const store = loadStore();
|
|
285
|
+
if (store.wallets[opts.name]) {
|
|
286
|
+
console.error(chalk.red(`\n Wallet "${opts.name}" already exists.\n`));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
store.wallets[opts.name] = {
|
|
290
|
+
name: opts.name, type: "evm", address, privateKey: pk,
|
|
291
|
+
createdAt: new Date().toISOString(),
|
|
292
|
+
};
|
|
293
|
+
if (!store.active.hyperliquid)
|
|
294
|
+
store.active.hyperliquid = opts.name;
|
|
295
|
+
if (!store.active.lighter)
|
|
296
|
+
store.active.lighter = opts.name;
|
|
297
|
+
saveStore(store);
|
|
298
|
+
// Also write to .env so loadPrivateKey finds it immediately
|
|
299
|
+
setEnvVar("HL_PRIVATE_KEY", pk);
|
|
300
|
+
setEnvVar("LIGHTER_PRIVATE_KEY", pk);
|
|
301
|
+
if (isJson())
|
|
302
|
+
return printJson(jsonOk({ name: opts.name, type: "evm", address }));
|
|
303
|
+
console.log(chalk.cyan.bold("\n EVM Wallet Imported\n"));
|
|
304
|
+
console.log(` Name: ${chalk.white.bold(opts.name)}`);
|
|
305
|
+
console.log(` Address: ${chalk.green(address)}\n`);
|
|
306
|
+
});
|
|
307
|
+
// ── use (set active wallet for exchange) ──
|
|
308
|
+
wallet
|
|
309
|
+
.command("use <name> [exchange]")
|
|
310
|
+
.description("Set active wallet for exchange (auto-detects if omitted)")
|
|
311
|
+
.option("--for <exchange>", "Exchange to bind (legacy alias)")
|
|
312
|
+
.action(async (name, exchangeArg, opts) => {
|
|
313
|
+
const store = loadStore();
|
|
314
|
+
const entry = store.wallets[name];
|
|
315
|
+
if (!entry) {
|
|
316
|
+
console.error(chalk.red(`\n Wallet "${name}" not found. Run 'perp wallet list' to see available wallets.\n`));
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
// Resolve exchange: positional arg > --for flag > auto-detect from wallet type
|
|
320
|
+
const rawExchange = exchangeArg || opts.for;
|
|
321
|
+
let exchanges;
|
|
322
|
+
if (rawExchange) {
|
|
323
|
+
const ex = rawExchange.toLowerCase();
|
|
324
|
+
const needsSolana = ex === "pacifica";
|
|
325
|
+
if (needsSolana && entry.type !== "solana") {
|
|
326
|
+
console.error(chalk.red(`\n Pacifica requires a Solana wallet. "${name}" is EVM.\n`));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
if (!needsSolana && entry.type !== "evm") {
|
|
330
|
+
console.error(chalk.red(`\n ${ex} requires an EVM wallet. "${name}" is Solana.\n`));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
exchanges = [ex];
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Auto-detect from wallet type
|
|
337
|
+
exchanges = entry.type === "solana" ? ["pacifica"] : ["hyperliquid", "lighter"];
|
|
338
|
+
}
|
|
339
|
+
for (const exchange of exchanges) {
|
|
340
|
+
store.active[exchange] = name;
|
|
341
|
+
}
|
|
342
|
+
saveStore(store);
|
|
343
|
+
if (isJson())
|
|
344
|
+
return printJson(jsonOk({ exchanges, wallet: name, address: entry.address }));
|
|
345
|
+
console.log(chalk.green(`\n ${chalk.white.bold(name)} is now active for ${chalk.cyan(exchanges.join(", "))}`));
|
|
346
|
+
console.log(chalk.gray(` Address: ${entry.address}\n`));
|
|
347
|
+
});
|
|
348
|
+
// ── set (configure exchange key → ~/.perp/.env) ──
|
|
349
|
+
wallet
|
|
350
|
+
.command("set <exchange> <key>")
|
|
351
|
+
.description("Set private key for an exchange")
|
|
352
|
+
.option("--default", "Also set as default exchange")
|
|
353
|
+
.action(async (exchange, key, opts) => {
|
|
354
|
+
// Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter)
|
|
355
|
+
const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
|
|
356
|
+
const resolved = aliases[exchange.toLowerCase()] || exchange.toLowerCase();
|
|
357
|
+
const info = EXCHANGE_ENV_MAP[resolved];
|
|
358
|
+
if (!info) {
|
|
359
|
+
if (isJson()) {
|
|
360
|
+
const { jsonError } = await import("../utils.js");
|
|
361
|
+
return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter (or hl, pac, lt)`));
|
|
362
|
+
}
|
|
363
|
+
console.error(chalk.red(`\n Unknown exchange: ${exchange}`));
|
|
364
|
+
console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter (or hl, pac, lt)\n`));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
const { valid, address } = await validateKey(info.chain, key);
|
|
368
|
+
if (!valid) {
|
|
369
|
+
if (isJson()) {
|
|
370
|
+
const { jsonError } = await import("../utils.js");
|
|
371
|
+
return printJson(jsonError("INVALID_PARAMS", `Invalid ${info.chain} private key`));
|
|
372
|
+
}
|
|
373
|
+
console.error(chalk.red(`\n Invalid ${info.chain} private key.\n`));
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
const normalized = info.chain === "evm"
|
|
377
|
+
? (key.startsWith("0x") ? key : `0x${key}`)
|
|
378
|
+
: key;
|
|
379
|
+
setEnvVar(info.envKey, normalized);
|
|
380
|
+
if (opts.default) {
|
|
381
|
+
const settings = loadSettings();
|
|
382
|
+
settings.defaultExchange = resolved;
|
|
383
|
+
saveSettings(settings);
|
|
384
|
+
}
|
|
385
|
+
if (isJson())
|
|
386
|
+
return printJson(jsonOk({ exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default }));
|
|
387
|
+
console.log(chalk.green(`\n ${resolved} configured.`));
|
|
388
|
+
console.log(` Address: ${chalk.green(address)}`);
|
|
389
|
+
console.log(` Saved to: ${chalk.gray("~/.perp/.env")}`);
|
|
390
|
+
if (opts.default)
|
|
391
|
+
console.log(` Default: ${chalk.cyan(resolved)}`);
|
|
392
|
+
console.log();
|
|
393
|
+
});
|
|
394
|
+
// ── show (show configured exchanges with public addresses) ──
|
|
395
|
+
wallet
|
|
396
|
+
.command("show")
|
|
397
|
+
.description("Show configured wallets with public addresses")
|
|
398
|
+
.action(async () => {
|
|
399
|
+
const stored = loadEnvFile();
|
|
400
|
+
const entries = [];
|
|
401
|
+
for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
|
|
402
|
+
const fromFile = stored[info.envKey];
|
|
403
|
+
const fromEnv = process.env[info.envKey];
|
|
404
|
+
if (fromFile) {
|
|
405
|
+
entries.push({ name: exchange, chain: info.chain, key: fromFile, source: "~/.perp/.env" });
|
|
406
|
+
}
|
|
407
|
+
else if (fromEnv) {
|
|
408
|
+
entries.push({ name: exchange, chain: info.chain, key: fromEnv, source: "environment" });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const results = [];
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
const { valid, address } = await validateKey(entry.chain, entry.key);
|
|
414
|
+
results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
|
|
415
|
+
}
|
|
416
|
+
if (isJson()) {
|
|
417
|
+
const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
|
|
418
|
+
return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
|
|
419
|
+
}
|
|
420
|
+
console.log(chalk.cyan.bold("\n Configured Wallets\n"));
|
|
421
|
+
if (results.length === 0) {
|
|
422
|
+
console.log(chalk.gray(" No keys configured."));
|
|
423
|
+
console.log(chalk.gray(` Run ${chalk.cyan("perp init")} or ${chalk.cyan("perp wallet set <exchange> <key>")}\n`));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
for (const { name, address, source } of results) {
|
|
427
|
+
console.log(` ${chalk.cyan(name.padEnd(14))} ${chalk.green(address)} ${chalk.gray(source)}`);
|
|
428
|
+
}
|
|
429
|
+
console.log();
|
|
430
|
+
});
|
|
431
|
+
// ── list ──
|
|
432
|
+
wallet
|
|
433
|
+
.command("list")
|
|
434
|
+
.description("List all saved wallets (legacy wallets.json)")
|
|
435
|
+
.action(async () => {
|
|
436
|
+
const store = loadStore();
|
|
437
|
+
const entries = Object.values(store.wallets);
|
|
438
|
+
if (isJson())
|
|
439
|
+
return printJson(jsonOk({ wallets: store.wallets, active: store.active }));
|
|
440
|
+
if (entries.length === 0) {
|
|
441
|
+
console.log(chalk.gray("\n No wallets found."));
|
|
442
|
+
console.log(chalk.gray(" Use 'perp wallet generate' or 'perp wallet import' to add one.\n"));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
// Build reverse map: wallet name -> which exchanges it's active for
|
|
446
|
+
const activeMap = new Map();
|
|
447
|
+
for (const [exchange, walletName] of Object.entries(store.active)) {
|
|
448
|
+
if (!activeMap.has(walletName))
|
|
449
|
+
activeMap.set(walletName, []);
|
|
450
|
+
activeMap.get(walletName).push(exchange);
|
|
451
|
+
}
|
|
452
|
+
console.log(chalk.cyan.bold("\n Saved Wallets\n"));
|
|
453
|
+
const rows = entries.map((w) => {
|
|
454
|
+
const activeFor = activeMap.get(w.name) ?? [];
|
|
455
|
+
const activeStr = activeFor.length
|
|
456
|
+
? chalk.cyan(activeFor.join(", "))
|
|
457
|
+
: chalk.gray("-");
|
|
458
|
+
return [
|
|
459
|
+
chalk.white.bold(w.name),
|
|
460
|
+
w.type,
|
|
461
|
+
chalk.green(w.address.slice(0, 10) + "..." + w.address.slice(-6)),
|
|
462
|
+
activeStr,
|
|
463
|
+
chalk.gray(w.createdAt.split("T")[0]),
|
|
464
|
+
];
|
|
465
|
+
});
|
|
466
|
+
console.log(makeTable(["Name", "Type", "Address", "Active For", "Created"], rows));
|
|
467
|
+
console.log();
|
|
468
|
+
});
|
|
469
|
+
// ── remove ──
|
|
470
|
+
wallet
|
|
471
|
+
.command("remove <name>")
|
|
472
|
+
.description("Remove a saved wallet")
|
|
473
|
+
.action(async (name) => {
|
|
474
|
+
const store = loadStore();
|
|
475
|
+
if (!store.wallets[name]) {
|
|
476
|
+
console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
const address = store.wallets[name].address;
|
|
480
|
+
delete store.wallets[name];
|
|
481
|
+
// Clear active references
|
|
482
|
+
for (const [exchange, walletName] of Object.entries(store.active)) {
|
|
483
|
+
if (walletName === name)
|
|
484
|
+
delete store.active[exchange];
|
|
485
|
+
}
|
|
486
|
+
saveStore(store);
|
|
487
|
+
if (isJson())
|
|
488
|
+
return printJson(jsonOk({ removed: name, address }));
|
|
489
|
+
console.log(chalk.yellow(`\n Wallet "${name}" removed.`));
|
|
490
|
+
console.log(chalk.gray(` Address was: ${address}\n`));
|
|
491
|
+
});
|
|
492
|
+
// ── rename ──
|
|
493
|
+
wallet
|
|
494
|
+
.command("rename <oldName> <newName>")
|
|
495
|
+
.description("Rename a wallet")
|
|
496
|
+
.action(async (oldName, newName) => {
|
|
497
|
+
const store = loadStore();
|
|
498
|
+
if (!store.wallets[oldName]) {
|
|
499
|
+
console.error(chalk.red(`\n Wallet "${oldName}" not found.\n`));
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
if (store.wallets[newName]) {
|
|
503
|
+
console.error(chalk.red(`\n Wallet "${newName}" already exists.\n`));
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
store.wallets[newName] = { ...store.wallets[oldName], name: newName };
|
|
507
|
+
delete store.wallets[oldName];
|
|
508
|
+
// Update active references
|
|
509
|
+
for (const [exchange, walletName] of Object.entries(store.active)) {
|
|
510
|
+
if (walletName === oldName)
|
|
511
|
+
store.active[exchange] = newName;
|
|
512
|
+
}
|
|
513
|
+
saveStore(store);
|
|
514
|
+
if (isJson())
|
|
515
|
+
return printJson(jsonOk({ renamed: { from: oldName, to: newName } }));
|
|
516
|
+
console.log(chalk.green(`\n Renamed "${oldName}" -> "${newName}"\n`));
|
|
517
|
+
});
|
|
518
|
+
// ── balance (by wallet name) ──
|
|
519
|
+
wallet
|
|
520
|
+
.command("balance [name]")
|
|
521
|
+
.description("Check on-chain balance for a saved wallet (or active wallet)")
|
|
522
|
+
.option("--testnet", "Use testnet", false)
|
|
523
|
+
.action(async (name, opts) => {
|
|
524
|
+
const store = loadStore();
|
|
525
|
+
let entry;
|
|
526
|
+
if (name) {
|
|
527
|
+
entry = store.wallets[name];
|
|
528
|
+
if (!entry) {
|
|
529
|
+
console.error(chalk.red(`\n Wallet "${name}" not found.\n`));
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
// Show all active wallets
|
|
535
|
+
const activeEntries = Object.entries(store.active)
|
|
536
|
+
.map(([ex, wn]) => ({ exchange: ex, ...store.wallets[wn] }))
|
|
537
|
+
.filter((e) => e.address);
|
|
538
|
+
if (activeEntries.length === 0) {
|
|
539
|
+
console.error(chalk.gray("\n No active wallets. Use 'perp wallet use <name>' to set one.\n"));
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
for (const ae of activeEntries) {
|
|
543
|
+
if (!isJson())
|
|
544
|
+
console.log(chalk.cyan.bold(`\n ${ae.name} (${ae.exchange})`));
|
|
545
|
+
const balances = ae.type === "solana"
|
|
546
|
+
? await getSolanaBalances(ae.address, opts.testnet)
|
|
547
|
+
: await getEvmBalances(ae.address, opts.testnet);
|
|
548
|
+
if (isJson()) {
|
|
549
|
+
printJson(jsonOk({ wallet: ae.name, exchange: ae.exchange, balances }));
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const rows = balances.map((b) => [
|
|
553
|
+
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
554
|
+
]);
|
|
555
|
+
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
556
|
+
}
|
|
557
|
+
console.log();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!isJson())
|
|
561
|
+
console.log(chalk.cyan(`\n Fetching balance for "${entry.name}" (${entry.address.slice(0, 8)}...)\n`));
|
|
562
|
+
const balances = entry.type === "solana"
|
|
563
|
+
? await getSolanaBalances(entry.address, opts.testnet)
|
|
564
|
+
: await getEvmBalances(entry.address, opts.testnet);
|
|
565
|
+
if (isJson())
|
|
566
|
+
return printJson(jsonOk({ wallet: entry.name, balances }));
|
|
567
|
+
const rows = balances.map((b) => [
|
|
568
|
+
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
569
|
+
]);
|
|
570
|
+
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
571
|
+
console.log();
|
|
572
|
+
});
|
|
573
|
+
// ── direct address balance (kept for convenience) ──
|
|
574
|
+
wallet
|
|
575
|
+
.command("solana <address>")
|
|
576
|
+
.description("Check Solana wallet balances by address")
|
|
577
|
+
.option("--testnet", "Use devnet", false)
|
|
578
|
+
.action(async (address, opts) => {
|
|
579
|
+
if (!isJson())
|
|
580
|
+
console.log(chalk.cyan(`\n Fetching Solana balances for ${address.slice(0, 8)}...${address.slice(-4)}\n`));
|
|
581
|
+
const balances = await getSolanaBalances(address, opts.testnet);
|
|
582
|
+
if (isJson())
|
|
583
|
+
return printJson(jsonOk(balances));
|
|
584
|
+
const rows = balances.map((b) => [chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-")]);
|
|
585
|
+
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
586
|
+
console.log(chalk.gray(`\n Network: ${opts.testnet ? "Devnet" : "Mainnet"}\n`));
|
|
587
|
+
});
|
|
588
|
+
wallet
|
|
589
|
+
.command("arbitrum <address>")
|
|
590
|
+
.description("Check Arbitrum wallet balances by address")
|
|
591
|
+
.option("--testnet", "Use Sepolia testnet", false)
|
|
592
|
+
.action(async (address, opts) => {
|
|
593
|
+
if (!isJson())
|
|
594
|
+
console.log(chalk.cyan(`\n Fetching Arbitrum balances for ${address.slice(0, 8)}...${address.slice(-4)}\n`));
|
|
595
|
+
const balances = await getEvmBalances(address, opts.testnet);
|
|
596
|
+
if (isJson())
|
|
597
|
+
return printJson(jsonOk(balances));
|
|
598
|
+
const rows = balances.map((b) => [chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-")]);
|
|
599
|
+
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
600
|
+
console.log(chalk.gray(`\n Network: Arbitrum ${opts.testnet ? "Sepolia" : "One"}\n`));
|
|
601
|
+
});
|
|
602
|
+
}
|