perp-cli 0.12.17 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +303 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +357 -459
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +240 -6
- package/dist/exchanges/aster.js +1139 -195
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +95 -0
- package/dist/exchanges/ondo/funds.js +198 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1386 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +29 -0
- package/dist/exchanges/venues/ondo.js +136 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +39 -0
- package/dist/exchanges/venues/qfex.js +140 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
package/dist/commands/funds.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
|
+
import { isDryRun as dryRunFlag, supportsDryRun } from "../cli-flags.js";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import { printJson, formatUsd, jsonOk, jsonError } from "../utils.js";
|
|
3
4
|
import { logExecution } from "../execution-log.js";
|
|
4
5
|
import { hasPacificaSdk, hasEvmAddress, isWithdrawCapable, isUsdTransferCapable, hasLighterAccount } from "../exchanges/capabilities.js";
|
|
5
6
|
import { registerFundsBridgeCommands } from "./bridge.js";
|
|
6
7
|
import { registerFundsRebalanceCommands } from "./rebalance.js";
|
|
8
|
+
import { PerpError, isConnectFailure, isDefiniteOrderFailure, isTransportFailure, orderOutcomeUnknown, parsePositiveDecimal } from "../errors.js";
|
|
9
|
+
import { cliCredentialOptsFrom, requireMasterWallet } from "../config.js";
|
|
10
|
+
import { getVenue, requireCapabilities, READ_ONLY_CREDENTIALS } from "../exchanges/venue.js";
|
|
11
|
+
import { unsupported } from "../exchanges/trading-v2.js";
|
|
12
|
+
import { registerOnchainFundsCommands } from "./funds-onchain.js";
|
|
13
|
+
import { QFEX_BANKER_NETWORK, QFEX_WITHDRAW_DISABLED, QFEX_WITHDRAW_ENABLED, QFEX_WITHDRAW_NETWORK } from "../exchanges/venues/qfex.js";
|
|
14
|
+
import { loadSettings } from "../settings.js";
|
|
15
|
+
import { assertWithdrawAllowed, forgetWithdrawAttempt, recordWithdrawAttempt } from "../withdraw-gate.js";
|
|
16
|
+
import "../exchanges/venues/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Execution-log status of a failed funds action: an unknown outcome (ORDER_PENDING — the request
|
|
19
|
+
* was sent and may have executed) is logged "unverified", never "failed".
|
|
20
|
+
*/
|
|
21
|
+
export function fundsFailStatus(err) {
|
|
22
|
+
return err instanceof PerpError && err.structured.code === "ORDER_PENDING" ? "unverified" : "failed";
|
|
23
|
+
}
|
|
24
|
+
/** `{ txHash }` of a broadcast transaction whose outcome is unknown (ORDER_PENDING details), for the execution log. */
|
|
25
|
+
function pendingTxMeta(err) {
|
|
26
|
+
const h = err instanceof PerpError ? err.structured.details?.txHash : undefined;
|
|
27
|
+
return typeof h === "string" ? { txHash: h } : {};
|
|
28
|
+
}
|
|
7
29
|
const DEFAULT_RELAYER = "http://localhost:3100";
|
|
8
30
|
function getRelayerUrl() {
|
|
9
31
|
return process.env.PERP_RELAYER_URL || DEFAULT_RELAYER;
|
|
10
32
|
}
|
|
33
|
+
function relayerUnavailable() {
|
|
34
|
+
return new PerpError("EXCHANGE_UNREACHABLE", `The relayer at ${getRelayerUrl()} is not reachable; nothing was sent`, {
|
|
35
|
+
remediation: "Start the relayer / set PERP_RELAYER_URL, or drop --relay for a direct deposit (you pay gas)",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** A broadcast deposit whose confirmation could not be read: unknown outcome, carrying the signature. */
|
|
39
|
+
export function depositOutcomeUnknown(signature, e) {
|
|
40
|
+
const pending = orderOutcomeUnknown("pacifica", `deposit transaction ${signature}`, e, `Do NOT deposit again yet — the transaction was broadcast and may still confirm. Check signature ${signature} on a Solana explorer, then: perp -e pacifica account balance`);
|
|
41
|
+
return new PerpError("ORDER_PENDING", pending.message, { exchange: "pacifica", signature, remediation: pending.structured.remediation });
|
|
42
|
+
}
|
|
11
43
|
async function relayerAvailable() {
|
|
12
44
|
try {
|
|
13
45
|
const res = await fetch(`${getRelayerUrl()}/health`, { signal: AbortSignal.timeout(2000) });
|
|
@@ -17,30 +49,158 @@ async function relayerAvailable() {
|
|
|
17
49
|
return false;
|
|
18
50
|
}
|
|
19
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Local withdraw-gate verdict for a Hyperliquid destination that is not (known to be) the account
|
|
54
|
+
* owner: settings.withdraw.hyperliquid allowlist + maxPerTx / maxPerDay (the QFEX gate mechanism,
|
|
55
|
+
* src/withdraw-gate.ts). "passes" or "would refuse: …"; never throws a POLICY_DENIED (dry runs).
|
|
56
|
+
*/
|
|
57
|
+
export function hyperliquidGateVerdict(network, amount, destination, nowMs) {
|
|
58
|
+
const gate = loadSettings().withdraw.hyperliquid;
|
|
59
|
+
try {
|
|
60
|
+
assertWithdrawAllowed("hyperliquid", network, amount, destination, gate === undefined ? null : gate, nowMs);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (e instanceof PerpError && e.structured.code === "POLICY_DENIED")
|
|
64
|
+
return `would refuse: ${e.message}`;
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
return "passes (destination allowlisted in settings withdraw.hyperliquid, within maxPerTx / maxPerDay)";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Enforce settings.withdraw.hyperliquid for a master-signed withdraw3 / usdSend to `destination`
|
|
71
|
+
* and record the attempt in the withdraw ledger BEFORE it is sent. Returns the ledger id (forget it
|
|
72
|
+
* only on a definite refusal), or null when the destination is the owner (no gate, no ledger).
|
|
73
|
+
*/
|
|
74
|
+
export function hyperliquidGateEnforce(network, amount, destination, owner, nowMs) {
|
|
75
|
+
if (owner !== null && destination.toLowerCase() === owner.toLowerCase())
|
|
76
|
+
return null;
|
|
77
|
+
const gate = loadSettings().withdraw.hyperliquid;
|
|
78
|
+
assertWithdrawAllowed("hyperliquid", network, amount, destination, gate === undefined ? null : gate, nowMs);
|
|
79
|
+
return recordWithdrawAttempt("hyperliquid", network, amount, destination, nowMs);
|
|
80
|
+
}
|
|
81
|
+
/** Display names of `VenueNetworkConfig.settlementChain` / withdraw-network ids. */
|
|
82
|
+
const CHAIN_LABELS = {
|
|
83
|
+
ethereum: "Ethereum",
|
|
84
|
+
robinhood: "Robinhood Chain",
|
|
85
|
+
arbitrum: "Arbitrum",
|
|
86
|
+
solana: "Solana",
|
|
87
|
+
bnb: "BNB Chain",
|
|
88
|
+
hyperliquid: "Hyperliquid",
|
|
89
|
+
};
|
|
90
|
+
/** Label for a chain id. An unmapped id is a programming error (covered by a unit test over every venue). */
|
|
91
|
+
export function chainLabel(chain) {
|
|
92
|
+
const label = CHAIN_LABELS[chain];
|
|
93
|
+
if (label === undefined)
|
|
94
|
+
throw new Error(`no display label for chain "${chain}" (add it to CHAIN_LABELS)`);
|
|
95
|
+
return label;
|
|
96
|
+
}
|
|
97
|
+
/** Withdraw asset declared by the venue definition (capabilities.withdrawAssets[0]). */
|
|
98
|
+
function withdrawAssetOf(def) {
|
|
99
|
+
const assets = requireCapabilities(def).withdrawAssets;
|
|
100
|
+
if (assets === null || assets.length === 0) {
|
|
101
|
+
throw new Error(`${def.id} declares no withdrawAssets but has a withdraw command`);
|
|
102
|
+
}
|
|
103
|
+
return assets[0];
|
|
104
|
+
}
|
|
105
|
+
/** Withdrawal chain of venues that settle off-chain and pay out on a public chain. */
|
|
106
|
+
const OFFCHAIN_WITHDRAW_CHAIN = {
|
|
107
|
+
qfex: QFEX_WITHDRAW_NETWORK,
|
|
108
|
+
};
|
|
109
|
+
/** Chain a venue's withdrawals arrive on (QFEX settles off-chain and pays out on Arbitrum). */
|
|
110
|
+
function withdrawChainOf(def, net) {
|
|
111
|
+
const payout = OFFCHAIN_WITHDRAW_CHAIN[def.id];
|
|
112
|
+
if (payout !== undefined)
|
|
113
|
+
return payout;
|
|
114
|
+
const cfg = def.networks[net];
|
|
115
|
+
if (cfg === null) {
|
|
116
|
+
throw new PerpError("NOT_IMPLEMENTED", `${def.displayName} has no ${net} environment`, {
|
|
117
|
+
remediation: `Run without --network testnet for ${def.id}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return cfg.settlementChain;
|
|
121
|
+
}
|
|
122
|
+
/** "-e lighter: USDC → Ethereum; -e lighter-rh: USDG → Robinhood Chain" — from the venue definitions. */
|
|
123
|
+
export function lighterFamilyWithdrawSummary() {
|
|
124
|
+
return ["lighter", "lighter-rh"].map(id => {
|
|
125
|
+
const def = getVenue(id);
|
|
126
|
+
return `-e ${id}: ${withdrawAssetOf(def)} → ${chainLabel(def.networks.mainnet.settlementChain)}`;
|
|
127
|
+
}).join("; ");
|
|
128
|
+
}
|
|
20
129
|
export function registerFundsCommands(program, getAdapter, isJson, getNetwork, getAdapterForExchange) {
|
|
21
130
|
const funds = program.command("funds").description("Fund movement — deposit, withdraw, transfer, bridge, rebalance");
|
|
131
|
+
/** Global --dry-run (root flag; never re-declared on a subcommand — the root would swallow it). */
|
|
132
|
+
const isDryRun = () => dryRunFlag(program);
|
|
133
|
+
/**
|
|
134
|
+
* Master-only fund movement: NO_SIGNER_AVAILABLE (do it on the website / import the master)
|
|
135
|
+
* when no master wallet is on this machine — runs after the dry-run return, before any key or
|
|
136
|
+
* adapter is loaded. An imported agent / API key never signs these.
|
|
137
|
+
*/
|
|
138
|
+
const requireMaster = (venue, action) => requireMasterWallet(venue, cliCredentialOptsFrom(program.opts()), action);
|
|
139
|
+
/**
|
|
140
|
+
* Dry-run exit for a fund movement: log + print the plan (`sent: false`) and send nothing.
|
|
141
|
+
* Every caller returns this BEFORE loading a key, building an adapter, or contacting a relayer.
|
|
142
|
+
*/
|
|
143
|
+
const printFundsPlan = (plan) => {
|
|
144
|
+
logExecution({
|
|
145
|
+
type: "bridge", exchange: plan.venue, symbol: plan.asset, side: plan.action,
|
|
146
|
+
size: plan.amount, status: "simulated", dryRun: true, meta: { ...plan },
|
|
147
|
+
});
|
|
148
|
+
if (isJson()) {
|
|
149
|
+
printJson(jsonOk({ dryRun: true, sent: false, ...plan }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.log(chalk.yellow(`\n [dry-run] Would ${plan.action} ${plan.amount} ${plan.asset} (${plan.venue}, ${plan.network})`));
|
|
153
|
+
for (const [k, v] of Object.entries(plan)) {
|
|
154
|
+
if (["action", "venue", "network", "asset", "amount"].includes(k))
|
|
155
|
+
continue;
|
|
156
|
+
console.log(` ${k}: ${String(v)}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Deposit routes whose chain RPC / bridge contract / token addresses are
|
|
162
|
+
* mainnet deployments (hardcoded below) have no testnet equivalent: a -n
|
|
163
|
+
* testnet run must fail before any key is loaded, never sign on mainnet.
|
|
164
|
+
*/
|
|
165
|
+
const requireMainnetDepositRoute = (route) => {
|
|
166
|
+
const network = getNetwork();
|
|
167
|
+
if (network === "mainnet")
|
|
168
|
+
return;
|
|
169
|
+
throw new PerpError("NO_BRIDGE_ROUTE", `${route} has no ${network} route — its chain RPC and contracts are mainnet deployments`, {
|
|
170
|
+
remediation: "Run without -n testnet to deposit on mainnet, or fund the testnet account from the venue's testnet faucet",
|
|
171
|
+
});
|
|
172
|
+
};
|
|
22
173
|
// ═══════════════════════════════════════════════════════
|
|
23
174
|
// DEPOSIT
|
|
24
175
|
// ═══════════════════════════════════════════════════════
|
|
25
176
|
const deposit = funds.command("deposit").description("Deposit funds into exchange accounts");
|
|
26
177
|
// ── Pacifica (Solana) ──
|
|
27
|
-
deposit
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.option("--no-relay", "
|
|
31
|
-
.action(async (amount,
|
|
32
|
-
const
|
|
178
|
+
supportsDryRun(deposit.command("pacifica <amount>"))
|
|
179
|
+
.description("Deposit USDC into Pacifica (supports --dry-run)")
|
|
180
|
+
.option("--relay", "Opt in to the gas-sponsoring relayer (PERP_RELAYER_URL); default: direct deposit, you pay gas")
|
|
181
|
+
.option("--no-relay", "Direct deposit, pay gas yourself (the default)")
|
|
182
|
+
.action(async (amount, rawOpts) => {
|
|
183
|
+
const opts = { relay: rawOpts.relay === true };
|
|
184
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
33
185
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
34
186
|
throw new Error("Invalid amount");
|
|
35
187
|
if (amountNum < 10)
|
|
36
188
|
throw new Error("Minimum deposit is 10 USDC");
|
|
37
|
-
|
|
38
|
-
|
|
189
|
+
if (isDryRun()) {
|
|
190
|
+
return printFundsPlan({ action: "deposit", venue: "pacifica", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: "solana", method: opts.relay ? "relayer" : "direct" });
|
|
191
|
+
}
|
|
192
|
+
await requireMaster("pacifica", "Pacifica deposit");
|
|
193
|
+
// Relayer (gas-sponsored) only when asked for with --relay; an unreachable relayer is an
|
|
194
|
+
// error, never a silent switch to a direct deposit.
|
|
195
|
+
if (opts.relay && !(await relayerAvailable()))
|
|
196
|
+
throw relayerUnavailable();
|
|
197
|
+
if (opts.relay) {
|
|
39
198
|
if (!isJson())
|
|
40
199
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} via relayer (gasless)...\n`));
|
|
41
|
-
|
|
200
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
201
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
42
202
|
if (adapter.name !== "pacifica")
|
|
43
|
-
throw new Error("
|
|
203
|
+
throw new Error("pacifica venue did not produce a Pacifica adapter");
|
|
44
204
|
const { PacificaAdapter } = await import("../exchanges/pacifica.js");
|
|
45
205
|
const pacAdapter = adapter;
|
|
46
206
|
try {
|
|
@@ -48,7 +208,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
48
208
|
method: "POST",
|
|
49
209
|
headers: { "Content-Type": "application/json" },
|
|
50
210
|
body: JSON.stringify({
|
|
51
|
-
userPubkey: pacAdapter.
|
|
211
|
+
userPubkey: pacAdapter.masterSolanaSigner().getPublicKeyBase58(),
|
|
52
212
|
amount: amountNum,
|
|
53
213
|
testnet: getNetwork() === "testnet",
|
|
54
214
|
}),
|
|
@@ -57,14 +217,31 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
57
217
|
const err = await buildRes.json();
|
|
58
218
|
throw new Error(err.error || "Relayer build failed");
|
|
59
219
|
}
|
|
60
|
-
const
|
|
220
|
+
const built = (await buildRes.json());
|
|
221
|
+
if (typeof built.transaction !== "string" || typeof built.fee !== "number" || typeof built.netAmount !== "number") {
|
|
222
|
+
throw new PerpError("POLICY_DENIED", "The relayer's build response is missing transaction / fee / netAmount; nothing was signed", {
|
|
223
|
+
remediation: "Drop --relay for a direct deposit (you pay gas)",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const { transaction, fee, netAmount } = built;
|
|
61
227
|
if (!isJson()) {
|
|
62
228
|
console.log(` Fee: $${formatUsd(fee)} (gas sponsored)`);
|
|
63
229
|
console.log(` Net: $${formatUsd(netAmount)} to Pacifica\n`);
|
|
64
230
|
}
|
|
65
231
|
const { Transaction } = await import("@solana/web3.js");
|
|
232
|
+
const { signSolanaTransaction } = await import("../pacifica/sign-tx.js");
|
|
66
233
|
const tx = Transaction.from(Buffer.from(transaction, "base64"));
|
|
67
|
-
|
|
234
|
+
// Never blind-sign a relayer-built transaction with the master key: it must be exactly the
|
|
235
|
+
// pinned Pacifica deposit (+ compute budget, + the declared, capped fee transfer) — else refuse.
|
|
236
|
+
const { assertRelayedDepositTransaction } = await import("../pacifica/deposit.js");
|
|
237
|
+
const { PublicKey: SolPublicKey } = await import("@solana/web3.js");
|
|
238
|
+
await assertRelayedDepositTransaction(tx, {
|
|
239
|
+
user: new SolPublicKey(pacAdapter.masterSolanaSigner().getPublicKeyBase58()),
|
|
240
|
+
requestedAmount: amount.trim(), network: getNetwork(),
|
|
241
|
+
declared: { fee, netAmount, feeRecipient: typeof built.feeRecipient === "string" ? built.feeRecipient : null },
|
|
242
|
+
});
|
|
243
|
+
// The relayer pays the fee and adds its signature after ours.
|
|
244
|
+
signSolanaTransaction(pacAdapter.masterSolanaSigner(), tx, true);
|
|
68
245
|
const signed = tx.serialize().toString("base64");
|
|
69
246
|
const submitRes = await fetch(`${getRelayerUrl()}/deposit/pacifica/submit`, {
|
|
70
247
|
method: "POST",
|
|
@@ -94,7 +271,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
94
271
|
catch (err) {
|
|
95
272
|
logExecution({
|
|
96
273
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
|
|
97
|
-
size: String(amountNum), status:
|
|
274
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
98
275
|
error: err instanceof Error ? err.message : String(err),
|
|
99
276
|
meta: { action: "deposit", method: "relay" },
|
|
100
277
|
});
|
|
@@ -106,9 +283,10 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
106
283
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Pacifica...\n`));
|
|
107
284
|
console.log(chalk.gray(" Requires: SOL for gas (~0.005 SOL) + USDC on Solana\n"));
|
|
108
285
|
}
|
|
109
|
-
|
|
286
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
287
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
110
288
|
if (adapter.name !== "pacifica")
|
|
111
|
-
throw new Error("
|
|
289
|
+
throw new Error("pacifica venue did not produce a Pacifica adapter");
|
|
112
290
|
const { PacificaAdapter: PacAdapterCls } = await import("../exchanges/pacifica.js");
|
|
113
291
|
const pacDirect = adapter;
|
|
114
292
|
try {
|
|
@@ -117,7 +295,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
117
295
|
const network = getNetwork();
|
|
118
296
|
const rpcUrl = network === "testnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
|
|
119
297
|
const connection = new Connection(rpcUrl, "confirmed");
|
|
120
|
-
const
|
|
298
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
299
|
+
const { getNetworkConfig } = await import("../pacifica/constants.js");
|
|
300
|
+
const { signSolanaTransaction } = await import("../pacifica/sign-tx.js");
|
|
301
|
+
const masterSigner = pacDirect.masterSolanaSigner();
|
|
302
|
+
const owner = new PublicKey(masterSigner.getPublicKeyBase58());
|
|
303
|
+
const solBalance = await connection.getBalance(owner);
|
|
121
304
|
if (solBalance < 5_000_000) {
|
|
122
305
|
if (isJson()) {
|
|
123
306
|
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient SOL for gas. Need ~0.005 SOL."));
|
|
@@ -128,9 +311,9 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
128
311
|
}
|
|
129
312
|
process.exit(1);
|
|
130
313
|
}
|
|
131
|
-
|
|
132
|
-
const USDC_MINT = new PublicKey(
|
|
133
|
-
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
|
|
314
|
+
// Testnet collateral is the devnet USDP mint, mainnet is USDC (pacifica/constants.ts).
|
|
315
|
+
const USDC_MINT = new PublicKey(getNetworkConfig(network).usdcMint);
|
|
316
|
+
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(owner, { mint: USDC_MINT });
|
|
134
317
|
const usdcBalance = tokenAccounts.value.length > 0
|
|
135
318
|
? tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount ?? 0
|
|
136
319
|
: 0;
|
|
@@ -143,13 +326,38 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
143
326
|
}
|
|
144
327
|
process.exit(1);
|
|
145
328
|
}
|
|
146
|
-
const ix = await buildDepositInstruction(
|
|
329
|
+
const ix = await buildDepositInstruction(owner, amount.trim(), network);
|
|
147
330
|
const tx = new Transaction().add(ix);
|
|
148
|
-
tx.feePayer =
|
|
331
|
+
tx.feePayer = owner;
|
|
149
332
|
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
|
|
150
|
-
tx
|
|
151
|
-
|
|
152
|
-
|
|
333
|
+
signSolanaTransaction(masterSigner, tx, false);
|
|
334
|
+
// The signature is known before the send: a transport failure after the request left
|
|
335
|
+
// (reset, timeout — not a refused connection) is an unknown outcome carrying it.
|
|
336
|
+
const signedSig = tx.signature;
|
|
337
|
+
let sig;
|
|
338
|
+
try {
|
|
339
|
+
sig = await connection.sendRawTransaction(tx.serialize());
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
if (signedSig !== null && isTransportFailure(e) && !isConnectFailure(e)) {
|
|
343
|
+
throw depositOutcomeUnknown((await import("bs58")).default.encode(signedSig), e);
|
|
344
|
+
}
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
// Broadcast: from here a confirmation timeout / RPC error is an UNKNOWN outcome (the
|
|
348
|
+
// transaction may still land) — reported with the signature, never as "failed".
|
|
349
|
+
let confirmation;
|
|
350
|
+
try {
|
|
351
|
+
confirmation = await connection.confirmTransaction(sig, "confirmed");
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
throw depositOutcomeUnknown(sig, e);
|
|
355
|
+
}
|
|
356
|
+
if (confirmation?.value?.err !== undefined && confirmation.value.err !== null) {
|
|
357
|
+
throw new PerpError("ACTION_REJECTED", `Pacifica deposit transaction ${sig} failed on-chain: ${JSON.stringify(confirmation.value.err)} — nothing was deposited`, {
|
|
358
|
+
exchange: "pacifica", signature: sig,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
153
361
|
logExecution({
|
|
154
362
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
|
|
155
363
|
size: String(amountNum), status: "success", dryRun: false,
|
|
@@ -162,45 +370,52 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
162
370
|
console.log(` Signature: ${sig}\n`);
|
|
163
371
|
}
|
|
164
372
|
catch (err) {
|
|
373
|
+
const pendingSig = err instanceof PerpError ? err.structured.details?.signature : undefined;
|
|
165
374
|
logExecution({
|
|
166
375
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
|
|
167
|
-
size: String(amountNum), status:
|
|
376
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
168
377
|
error: err instanceof Error ? err.message : String(err),
|
|
169
|
-
meta: { action: "deposit", method: "direct" },
|
|
378
|
+
meta: { action: "deposit", method: "direct", ...(typeof pendingSig === "string" ? { signature: pendingSig } : {}) },
|
|
170
379
|
});
|
|
171
380
|
throw err;
|
|
172
381
|
}
|
|
173
382
|
});
|
|
174
383
|
// ── Hyperliquid (Arbitrum — USDC transfer to Bridge2) ──
|
|
175
|
-
deposit
|
|
176
|
-
.
|
|
177
|
-
.
|
|
178
|
-
.option("--no-relay", "
|
|
179
|
-
.action(async (amount,
|
|
180
|
-
const
|
|
384
|
+
supportsDryRun(deposit.command("hyperliquid <amount>"))
|
|
385
|
+
.description("Deposit USDC into Hyperliquid (Arbitrum; supports --dry-run)")
|
|
386
|
+
.option("--relay", "Opt in to the gas-sponsoring relayer (PERP_RELAYER_URL); default: direct deposit, you pay gas")
|
|
387
|
+
.option("--no-relay", "Direct deposit, pay gas yourself (the default)")
|
|
388
|
+
.action(async (amount, rawOpts) => {
|
|
389
|
+
const opts = { relay: rawOpts.relay === true };
|
|
390
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
181
391
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
182
392
|
throw new Error("Invalid amount");
|
|
183
393
|
if (amountNum < 5)
|
|
184
394
|
throw new Error("Minimum deposit is $5 for Hyperliquid");
|
|
185
|
-
//
|
|
186
|
-
|
|
395
|
+
// Relayer and direct Bridge2 transfer both target Arbitrum One + the mainnet HL bridge.
|
|
396
|
+
requireMainnetDepositRoute("Hyperliquid deposit (Arbitrum Bridge2 / relayer)");
|
|
397
|
+
if (isDryRun()) {
|
|
398
|
+
return printFundsPlan({ action: "deposit", venue: "hyperliquid", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: "arbitrum", method: opts.relay ? "relayer" : "USDC transfer to Bridge2" });
|
|
399
|
+
}
|
|
400
|
+
await requireMaster("hyperliquid", "Hyperliquid deposit");
|
|
401
|
+
// Relayer only when asked for with --relay; an unreachable relayer is an error.
|
|
402
|
+
if (opts.relay && !(await relayerAvailable()))
|
|
403
|
+
throw relayerUnavailable();
|
|
404
|
+
// The depositing wallet is the OWS master (no raw key): its address for the relayer, its
|
|
405
|
+
// transaction signer for the direct Bridge2 transfer.
|
|
406
|
+
const evm = await import("../evm-master-tx.js");
|
|
407
|
+
const signer = await evm.loadMasterEvmSigner("hyperliquid", cliCredentialOptsFrom(program.opts()), "Hyperliquid deposit");
|
|
408
|
+
const depositor = signer.getAddress();
|
|
409
|
+
if (opts.relay) {
|
|
187
410
|
if (!isJson())
|
|
188
411
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} via relayer (gasless)...\n`));
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let pk;
|
|
192
|
-
try {
|
|
193
|
-
pk = await loadPrivateKey("hyperliquid");
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
throw new Error(`Private key not configured for Hyperliquid. Run: perp setup`);
|
|
197
|
-
}
|
|
198
|
-
const wallet = new ethers.Wallet(pk);
|
|
412
|
+
// The relayer is given the account address and the amount only. Nothing it returns is
|
|
413
|
+
// ever signed with the master key on this path.
|
|
199
414
|
try {
|
|
200
415
|
const res = await fetch(`${getRelayerUrl()}/deposit/hyperliquid`, {
|
|
201
416
|
method: "POST",
|
|
202
417
|
headers: { "Content-Type": "application/json" },
|
|
203
|
-
body: JSON.stringify({ userAddress:
|
|
418
|
+
body: JSON.stringify({ userAddress: depositor, amount: amountNum }),
|
|
204
419
|
});
|
|
205
420
|
const result = (await res.json());
|
|
206
421
|
if (!res.ok)
|
|
@@ -222,7 +437,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
222
437
|
catch (err) {
|
|
223
438
|
logExecution({
|
|
224
439
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
|
|
225
|
-
size: String(amountNum), status:
|
|
440
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
226
441
|
error: err instanceof Error ? err.message : String(err),
|
|
227
442
|
meta: { action: "deposit", method: "relay" },
|
|
228
443
|
});
|
|
@@ -234,150 +449,140 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
234
449
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Hyperliquid...\n`));
|
|
235
450
|
console.log(chalk.gray(" Requires: ETH on Arbitrum for gas (~0.0001 ETH) + USDC on Arbitrum\n"));
|
|
236
451
|
}
|
|
237
|
-
const { ethers } = await import("ethers");
|
|
238
|
-
const { loadPrivateKey } = await import("../config.js");
|
|
239
|
-
const pk = await loadPrivateKey("hyperliquid");
|
|
240
|
-
const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
|
|
241
|
-
const wallet = new ethers.Wallet(pk, provider);
|
|
242
452
|
const HL_BRIDGE = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7";
|
|
243
453
|
const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
console.error(chalk.red(" Insufficient ETH on Arbitrum for gas."));
|
|
251
|
-
}
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
254
|
-
const usdc = new ethers.Contract(USDC_ARB, [
|
|
255
|
-
"function transfer(address to, uint256 amount) returns (bool)",
|
|
256
|
-
"function balanceOf(address) view returns (uint256)",
|
|
257
|
-
], wallet);
|
|
258
|
-
const amountRaw = ethers.parseUnits(amount, 6);
|
|
259
|
-
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
454
|
+
const ARB_RPC = "https://arb1.arbitrum.io/rpc";
|
|
455
|
+
const { parseUnits, formatUnits } = await import("ethers");
|
|
456
|
+
const rpc = evm.evmProvider(ARB_RPC, 42161);
|
|
457
|
+
await evm.assertRpcChainId(rpc, ARB_RPC, 42161, "hyperliquid");
|
|
458
|
+
const amountRaw = parseUnits(amount, await evm.erc20Decimals(rpc, USDC_ARB));
|
|
459
|
+
const usdcBal = await evm.erc20Balance(rpc, USDC_ARB, depositor);
|
|
260
460
|
if (usdcBal < amountRaw) {
|
|
261
|
-
|
|
262
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
console.error(chalk.red(` Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
266
|
-
}
|
|
267
|
-
process.exit(1);
|
|
461
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient USDC on Arbitrum. Have: $${formatUsd(Number(formatUnits(usdcBal, 6)))}`, { exchange: "hyperliquid" });
|
|
268
462
|
}
|
|
269
463
|
try {
|
|
270
464
|
if (!isJson())
|
|
271
465
|
console.log(chalk.gray(" Sending USDC to Hyperliquid Bridge2..."));
|
|
272
|
-
|
|
273
|
-
|
|
466
|
+
// Simulated, signed by the OWS master, broadcast; a lost response / receipt timeout after
|
|
467
|
+
// the broadcast is ORDER_PENDING carrying the tx hash (never a retryable error).
|
|
468
|
+
const { txHash } = await evm.sendMasterTx(rpc, {
|
|
469
|
+
venue: "hyperliquid", chainLabel: "Arbitrum", chainId: 42161, from: depositor, to: USDC_ARB, value: 0n,
|
|
470
|
+
data: evm.ERC20.encodeFunctionData("transfer", [HL_BRIDGE, amountRaw]), label: "USDC transfer to Hyperliquid Bridge2",
|
|
471
|
+
}, tx => signer.signTransaction(tx));
|
|
274
472
|
logExecution({
|
|
275
473
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
|
|
276
474
|
size: String(amountNum), status: "success", dryRun: false,
|
|
277
|
-
meta: { action: "deposit", method: "direct", txHash
|
|
475
|
+
meta: { action: "deposit", method: "direct", txHash },
|
|
278
476
|
});
|
|
279
477
|
if (isJson())
|
|
280
|
-
return printJson(jsonOk({ txHash
|
|
478
|
+
return printJson(jsonOk({ txHash, amount: amountNum, method: "direct" }));
|
|
281
479
|
console.log(chalk.green(` Deposit confirmed!`));
|
|
282
480
|
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
283
|
-
console.log(` TX Hash: ${
|
|
481
|
+
console.log(` TX Hash: ${txHash}`);
|
|
284
482
|
console.log(chalk.gray(`\n Funds appear in ~1-3 minutes.\n`));
|
|
285
483
|
}
|
|
286
484
|
catch (err) {
|
|
287
485
|
logExecution({
|
|
288
486
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
|
|
289
|
-
size: String(amountNum), status:
|
|
487
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
290
488
|
error: err instanceof Error ? err.message : String(err),
|
|
291
|
-
meta: { action: "deposit", method: "direct" },
|
|
489
|
+
meta: { action: "deposit", method: "direct", ...pendingTxMeta(err) },
|
|
292
490
|
});
|
|
293
491
|
throw err;
|
|
294
492
|
}
|
|
295
493
|
});
|
|
296
494
|
// ── Lighter ──
|
|
297
495
|
const lighterDeposit = deposit.command("lighter").description("Deposit USDC into Lighter");
|
|
298
|
-
lighterDeposit
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
496
|
+
supportsDryRun(lighterDeposit.command("ethereum <amount>"))
|
|
497
|
+
.description("Deposit USDC via Ethereum L1 (min 1 USDC, gas: $3-10+; supports --dry-run)")
|
|
498
|
+
// Asset index comes from GET /api/v1/assetDetails, which the deposit docs
|
|
499
|
+
// name as the source for the contract's `_assetIndex`. Live registry:
|
|
500
|
+
// 3 = USDC (l1_decimals 6), 2 = LIT (l1_decimals 18), 1 = ETH. The old
|
|
501
|
+
// "2 = USDC" default contradicted this function's own USDC ERC20 transfer
|
|
502
|
+
// and its parseUnits(amount, 6). Same defect as the withdraw side, which
|
|
503
|
+
// was corrected to 3 in 966ffc0; the deposit side was missed then.
|
|
504
|
+
.option("--asset-id <id>", "Asset index (default: 3 = USDC, per /api/v1/assetDetails)", "3")
|
|
302
505
|
.option("--route <type>", "Route: 0=perps, 1=spot (default: 0)", "0")
|
|
303
506
|
.action(async (amount, opts) => {
|
|
304
|
-
const amountNum =
|
|
507
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
305
508
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
306
509
|
throw new Error("Invalid amount");
|
|
307
510
|
if (amountNum < 1)
|
|
308
511
|
throw new Error("Minimum deposit via Ethereum is 1 USDC");
|
|
512
|
+
// deposit(address _to, uint16 _assetIndex, uint8 _routeType, uint256 _amount) — Lighter docs
|
|
513
|
+
// (deposits-transfers-and-withdrawals): "`_routeType`: whether you want to deposit the asset
|
|
514
|
+
// to your perps (0), or spot account (1)". Strict: plain integers only ("3abc", "1e1", "" refused).
|
|
515
|
+
if (!/^\d+$/.test(opts.assetId) || Number(opts.assetId) > 65_535) {
|
|
516
|
+
throw new PerpError("INVALID_PARAMS", `--asset-id must be an integer asset index 0-65535 (got "${opts.assetId}")`, {
|
|
517
|
+
exchange: "lighter", remediation: "Use the asset index from GET /api/v1/assetDetails (3 = USDC)",
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (opts.route !== "0" && opts.route !== "1") {
|
|
521
|
+
throw new PerpError("INVALID_PARAMS", `--route must be 0 (perps) or 1 (spot) (got "${opts.route}")`, { exchange: "lighter", remediation: "Pass --route 0 or --route 1" });
|
|
522
|
+
}
|
|
523
|
+
const assetIndex = Number(opts.assetId);
|
|
524
|
+
const routeType = Number(opts.route);
|
|
525
|
+
requireMainnetDepositRoute("Lighter deposit via Ethereum L1");
|
|
526
|
+
if (isDryRun()) {
|
|
527
|
+
return printFundsPlan({ action: "deposit", venue: "lighter", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: "ethereum", contract: "0x3B4D794a66304f130a4db8f2551b0070dfcf5ca7", assetIndex: opts.assetId, routeType: opts.route, steps: "approve USDC (if allowance short) → deposit()" });
|
|
528
|
+
}
|
|
529
|
+
await requireMaster("lighter", "Lighter deposit");
|
|
309
530
|
if (!isJson()) {
|
|
310
531
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Lighter via Ethereum L1...\n`));
|
|
311
532
|
console.log(chalk.yellow(" Warning: Ethereum L1 gas fees may be high ($3-10+)\n"));
|
|
312
533
|
}
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const pk = await loadPrivateKey("lighter");
|
|
316
|
-
const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
|
|
317
|
-
const wallet = new ethers.Wallet(pk, provider);
|
|
318
|
-
const LIGHTER_CONTRACT = "0x3B4D794a66304f130a4db8f2551b0070dfcf5ca7";
|
|
534
|
+
// EIP-55 checksummed (the previous mixed-case literal failed ethers' checksum validation).
|
|
535
|
+
const LIGHTER_CONTRACT = "0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7";
|
|
319
536
|
const USDC_ETH = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
if (isJson()) {
|
|
332
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
console.error(chalk.red(" Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
|
|
336
|
-
}
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
537
|
+
const ETH_RPC = "https://eth.llamarpc.com";
|
|
538
|
+
const { Interface, parseUnits, formatUnits } = await import("ethers");
|
|
539
|
+
const evm = await import("../evm-master-tx.js");
|
|
540
|
+
// OWS master wallet signs both L1 transactions (no raw key).
|
|
541
|
+
const signer = await evm.loadMasterEvmSigner("lighter", cliCredentialOptsFrom(program.opts()), "Lighter deposit");
|
|
542
|
+
const depositor = signer.getAddress();
|
|
543
|
+
const zkLighter = new Interface(["function deposit(address _to, uint16 _assetIndex, uint8 _routeType, uint256 _amount) payable"]);
|
|
544
|
+
const rpc = evm.evmProvider(ETH_RPC, 1);
|
|
545
|
+
await evm.assertRpcChainId(rpc, ETH_RPC, 1, "lighter");
|
|
546
|
+
const amountRaw = parseUnits(amount, await evm.erc20Decimals(rpc, USDC_ETH));
|
|
547
|
+
const usdcBal = await evm.erc20Balance(rpc, USDC_ETH, depositor);
|
|
340
548
|
if (usdcBal < amountRaw) {
|
|
341
|
-
|
|
342
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
console.error(chalk.red(` Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
346
|
-
}
|
|
347
|
-
process.exit(1);
|
|
549
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient USDC on Ethereum. Have: $${formatUsd(Number(formatUnits(usdcBal, 6)))}`, { exchange: "lighter" });
|
|
348
550
|
}
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
if (!isJson())
|
|
352
|
-
console.log(chalk.gray(" Approving USDC for Lighter..."));
|
|
353
|
-
const approveTx = await usdc.approve(LIGHTER_CONTRACT, amountRaw);
|
|
354
|
-
await approveTx.wait();
|
|
355
|
-
if (!isJson())
|
|
356
|
-
console.log(chalk.gray(" Approved."));
|
|
357
|
-
}
|
|
358
|
-
if (!isJson())
|
|
359
|
-
console.log(chalk.gray(" Calling deposit()..."));
|
|
551
|
+
const base = { venue: "lighter", chainLabel: "Ethereum", chainId: 1, from: depositor, value: 0n };
|
|
552
|
+
const sign = (tx) => signer.signTransaction(tx);
|
|
360
553
|
try {
|
|
361
|
-
const
|
|
362
|
-
|
|
554
|
+
const allowance = await evm.erc20Allowance(rpc, USDC_ETH, depositor, LIGHTER_CONTRACT);
|
|
555
|
+
if (allowance < amountRaw) {
|
|
556
|
+
if (!isJson())
|
|
557
|
+
console.log(chalk.gray(" Approving USDC for Lighter..."));
|
|
558
|
+
await evm.sendMasterTx(rpc, { ...base, to: USDC_ETH, data: evm.ERC20.encodeFunctionData("approve", [LIGHTER_CONTRACT, amountRaw]), label: "USDC approve" }, sign);
|
|
559
|
+
if (!isJson())
|
|
560
|
+
console.log(chalk.gray(" Approved."));
|
|
561
|
+
}
|
|
562
|
+
if (!isJson())
|
|
563
|
+
console.log(chalk.gray(" Calling deposit()..."));
|
|
564
|
+
const { txHash } = await evm.sendMasterTx(rpc, {
|
|
565
|
+
...base, to: LIGHTER_CONTRACT, label: "Lighter L1 deposit",
|
|
566
|
+
data: zkLighter.encodeFunctionData("deposit", [depositor, assetIndex, routeType, amountRaw]),
|
|
567
|
+
}, sign);
|
|
363
568
|
logExecution({
|
|
364
569
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
365
570
|
size: String(amountNum), status: "success", dryRun: false,
|
|
366
|
-
meta: { action: "deposit", method: "ethereum", txHash
|
|
571
|
+
meta: { action: "deposit", method: "ethereum", txHash },
|
|
367
572
|
});
|
|
368
573
|
if (isJson())
|
|
369
|
-
return printJson(jsonOk({ txHash
|
|
574
|
+
return printJson(jsonOk({ txHash, amount: amountNum, chain: "ethereum", method: "direct" }));
|
|
370
575
|
console.log(chalk.green(`\n Deposit confirmed!`));
|
|
371
576
|
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
372
|
-
console.log(` TX Hash: ${
|
|
577
|
+
console.log(` TX Hash: ${txHash}`);
|
|
373
578
|
console.log(chalk.gray(`\n Funds appear in your Lighter perps account shortly.\n`));
|
|
374
579
|
}
|
|
375
580
|
catch (err) {
|
|
376
581
|
logExecution({
|
|
377
582
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
378
|
-
size: String(amountNum), status:
|
|
583
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
379
584
|
error: err instanceof Error ? err.message : String(err),
|
|
380
|
-
meta: { action: "deposit", method: "ethereum" },
|
|
585
|
+
meta: { action: "deposit", method: "ethereum", ...pendingTxMeta(err) },
|
|
381
586
|
});
|
|
382
587
|
throw err;
|
|
383
588
|
}
|
|
@@ -387,11 +592,10 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
387
592
|
base: { chainId: 8453, rpc: "https://mainnet.base.org", usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "Base" },
|
|
388
593
|
avalanche: { chainId: 43114, rpc: "https://api.avax.network/ext/bc/C/rpc", usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", name: "Avalanche" },
|
|
389
594
|
};
|
|
390
|
-
lighterDeposit
|
|
391
|
-
.
|
|
392
|
-
.description("Deposit USDC via CCTP (arbitrum, base, avalanche — min 5 USDC)")
|
|
595
|
+
supportsDryRun(lighterDeposit.command("cctp <chain> <amount>"))
|
|
596
|
+
.description("Deposit USDC via CCTP (arbitrum, base, avalanche — min 5 USDC; supports --dry-run)")
|
|
393
597
|
.action(async (chain, amount) => {
|
|
394
|
-
const amountNum =
|
|
598
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
395
599
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
396
600
|
throw new Error("Invalid amount");
|
|
397
601
|
if (amountNum < 5)
|
|
@@ -400,84 +604,88 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
400
604
|
const chainInfo = CCTP_CHAINS[chainKey];
|
|
401
605
|
if (!chainInfo)
|
|
402
606
|
throw new Error(`Unsupported chain: ${chain}. Use: arbitrum, base, avalanche`);
|
|
607
|
+
// The intent address comes from the -n adapter, but the USDC transfer is on a mainnet chain.
|
|
608
|
+
requireMainnetDepositRoute(`Lighter CCTP deposit (${chainInfo.name})`);
|
|
609
|
+
if (isDryRun()) {
|
|
610
|
+
return printFundsPlan({ action: "deposit", venue: "lighter", network: getNetwork(), asset: "USDC", amount: String(amountNum), chain: chainKey, method: "CCTP: create intent address → USDC transfer to it" });
|
|
611
|
+
}
|
|
612
|
+
await requireMaster("lighter", "Lighter CCTP deposit");
|
|
403
613
|
if (!isJson())
|
|
404
614
|
console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Lighter via CCTP (${chainInfo.name})...\n`));
|
|
405
|
-
const {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const
|
|
615
|
+
const { parseUnits, formatUnits } = await import("ethers");
|
|
616
|
+
const evm = await import("../evm-master-tx.js");
|
|
617
|
+
// OWS master wallet signs the USDC transfer (no raw key).
|
|
618
|
+
const signer = await evm.loadMasterEvmSigner("lighter", cliCredentialOptsFrom(program.opts()), "Lighter CCTP deposit");
|
|
619
|
+
const depositor = signer.getAddress();
|
|
410
620
|
if (!isJson())
|
|
411
621
|
console.log(chalk.gray(" Creating intent address..."));
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
})
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const gasBal = await provider.getBalance(wallet.address);
|
|
433
|
-
if (gasBal < ethers.parseEther("0.00005")) {
|
|
434
|
-
if (isJson()) {
|
|
435
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
console.error(chalk.red(` Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
|
|
622
|
+
// Network-aware: the adapter was built for the selected -n network (venue REST URL).
|
|
623
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
624
|
+
const lt = await getAdapterForExchange("lighter");
|
|
625
|
+
if (!(lt instanceof LighterAdapter))
|
|
626
|
+
throw new Error("lighter venue did not produce a LighterAdapter");
|
|
627
|
+
if (lt.address.toLowerCase() !== depositor.toLowerCase()) {
|
|
628
|
+
throw new PerpError("INVALID_PARAMS", `Lighter adapter address ${lt.address || "(none)"} differs from the depositing wallet ${depositor}`, {
|
|
629
|
+
remediation: "Run the deposit with the master wallet that owns the Lighter account (--ows <wallet>)",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// The USDC goes to this address: only the documented `intent_address` field is accepted
|
|
633
|
+
// (apidocs.lighter.xyz/reference/createintentaddress → CreateIntentAddressResp, required:
|
|
634
|
+
// "code", "intent_address"). The endpoint is unauthenticated and the docs offer no
|
|
635
|
+
// authenticated read of an account's intent address to cross-check against, so the address
|
|
636
|
+
// is requested twice and both answers must name the same address.
|
|
637
|
+
const readIntentAddress = async () => {
|
|
638
|
+
const intentData = (await lt.createIntentAddress(chainInfo.chainId));
|
|
639
|
+
const addr = intentData?.intent_address;
|
|
640
|
+
if (typeof addr !== "string" || !evm.EVM_ADDRESS_RE.test(addr) || /^0x0{40}$/.test(addr)) {
|
|
641
|
+
throw new PerpError("EXCHANGE_ERROR", `Lighter returned no intent_address for chain ${chainInfo.chainId} (got ${JSON.stringify(intentData).slice(0, 160)}) — nothing was sent`, { exchange: "lighter" });
|
|
439
642
|
}
|
|
440
|
-
|
|
643
|
+
return addr;
|
|
644
|
+
};
|
|
645
|
+
const intentAddress = await readIntentAddress();
|
|
646
|
+
const intentAddressAgain = await readIntentAddress();
|
|
647
|
+
if (intentAddress.toLowerCase() !== intentAddressAgain.toLowerCase()) {
|
|
648
|
+
throw new PerpError("EXCHANGE_ERROR", `Lighter returned two different intent addresses for chain ${chainInfo.chainId} (${intentAddress} then ${intentAddressAgain}) — nothing was sent`, {
|
|
649
|
+
exchange: "lighter",
|
|
650
|
+
remediation: "Do not send funds to either address. Retry later, or deposit from the Lighter web app",
|
|
651
|
+
});
|
|
441
652
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const amountRaw =
|
|
447
|
-
const usdcBal = await
|
|
653
|
+
if (!isJson())
|
|
654
|
+
console.log(` Intent address: ${intentAddress}`);
|
|
655
|
+
const rpc = evm.evmProvider(chainInfo.rpc, chainInfo.chainId);
|
|
656
|
+
await evm.assertRpcChainId(rpc, chainInfo.rpc, chainInfo.chainId, "lighter");
|
|
657
|
+
const amountRaw = parseUnits(amount, await evm.erc20Decimals(rpc, chainInfo.usdc));
|
|
658
|
+
const usdcBal = await evm.erc20Balance(rpc, chainInfo.usdc, depositor);
|
|
448
659
|
if (usdcBal < amountRaw) {
|
|
449
|
-
|
|
450
|
-
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
console.error(chalk.red(` Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
454
|
-
}
|
|
455
|
-
process.exit(1);
|
|
660
|
+
throw new PerpError("INSUFFICIENT_BALANCE", `Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(formatUnits(usdcBal, 6)))}`, { exchange: "lighter" });
|
|
456
661
|
}
|
|
457
662
|
if (!isJson())
|
|
458
663
|
console.log(chalk.gray(` Sending USDC to intent address on ${chainInfo.name}...`));
|
|
459
664
|
try {
|
|
460
|
-
const
|
|
461
|
-
|
|
665
|
+
const { txHash } = await evm.sendMasterTx(rpc, {
|
|
666
|
+
venue: "lighter", chainLabel: chainInfo.name, chainId: chainInfo.chainId, from: depositor, to: chainInfo.usdc, value: 0n,
|
|
667
|
+
data: evm.ERC20.encodeFunctionData("transfer", [intentAddress, amountRaw]), label: "USDC transfer to the Lighter CCTP intent address",
|
|
668
|
+
}, tx => signer.signTransaction(tx));
|
|
462
669
|
logExecution({
|
|
463
670
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
464
671
|
size: String(amountNum), status: "success", dryRun: false,
|
|
465
|
-
meta: { action: "deposit", method: "cctp", chain: chainKey, txHash
|
|
672
|
+
meta: { action: "deposit", method: "cctp", chain: chainKey, txHash },
|
|
466
673
|
});
|
|
467
674
|
if (isJson())
|
|
468
|
-
return printJson(jsonOk({ txHash
|
|
675
|
+
return printJson(jsonOk({ txHash, amount: amountNum, chain: chainKey, intentAddress, method: "cctp" }));
|
|
469
676
|
console.log(chalk.green(`\n USDC sent to intent address!`));
|
|
470
677
|
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
471
|
-
console.log(` TX Hash: ${
|
|
678
|
+
console.log(` TX Hash: ${txHash}`);
|
|
472
679
|
console.log(` Chain: ${chainInfo.name}`);
|
|
473
|
-
|
|
680
|
+
// apidocs.lighter.xyz/docs/create-accounts-programmatically: "Legacy CCTP route: around 15–20 minutes."
|
|
681
|
+
console.log(chalk.gray(`\n CCTP deposits take around 15-20 minutes to credit. Funds will appear in Lighter automatically.\n`));
|
|
474
682
|
}
|
|
475
683
|
catch (err) {
|
|
476
684
|
logExecution({
|
|
477
685
|
type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
|
|
478
|
-
size: String(amountNum), status:
|
|
686
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
479
687
|
error: err instanceof Error ? err.message : String(err),
|
|
480
|
-
meta: { action: "deposit", method: "cctp", chain: chainKey },
|
|
688
|
+
meta: { action: "deposit", method: "cctp", chain: chainKey, ...pendingTxMeta(err) },
|
|
481
689
|
});
|
|
482
690
|
throw err;
|
|
483
691
|
}
|
|
@@ -512,23 +720,157 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
512
720
|
console.log(` Command: ${chalk.green("perp funds deposit lighter cctp avalanche <amount>")}`);
|
|
513
721
|
console.log();
|
|
514
722
|
});
|
|
723
|
+
// ── Lighter RH (Robinhood Chain contract deposit) ──
|
|
724
|
+
{
|
|
725
|
+
const rhDef = getVenue("lighter-rh");
|
|
726
|
+
const rhAsset = rhDef.networks.mainnet.collateral[0];
|
|
727
|
+
const rhChain = chainLabel(rhDef.networks.mainnet.settlementChain);
|
|
728
|
+
supportsDryRun(deposit.command("lighter-rh <amount>"))
|
|
729
|
+
.description(`Deposit ${rhAsset} into ${rhDef.displayName} via the ${rhChain} contract (approve + deposit; min 1 ${rhAsset}; supports --dry-run)`)
|
|
730
|
+
.option("--route <type>", "Route: 0=perps, 1=spot (default: 0)", "0")
|
|
731
|
+
.action(async (amount, opts) => {
|
|
732
|
+
if (opts.route !== "0" && opts.route !== "1") {
|
|
733
|
+
throw new PerpError("INVALID_PARAMS", `--route must be 0 (perps) or 1 (spot), got "${opts.route}"`);
|
|
734
|
+
}
|
|
735
|
+
const net = getNetwork();
|
|
736
|
+
const { planLighterRhDeposit, executeLighterRhDeposit, requireLighterRhDepositNetwork } = await import("../exchanges/lighter-rh-deposit.js");
|
|
737
|
+
// Uniform CAPABILITY_UNSUPPORTED on testnet, before any credential is needed.
|
|
738
|
+
requireLighterRhDepositNetwork(net);
|
|
739
|
+
// The deposit is an L1 contract call from the master wallet; the plan needs its address too.
|
|
740
|
+
if (!isDryRun())
|
|
741
|
+
await requireMaster("lighter-rh", `${rhDef.displayName} deposit`);
|
|
742
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
743
|
+
const lt = await getAdapterForExchange("lighter-rh");
|
|
744
|
+
if (!(lt instanceof LighterAdapter))
|
|
745
|
+
throw new Error("lighter-rh venue did not produce a LighterAdapter");
|
|
746
|
+
const plan = await planLighterRhDeposit({ network: net, amount, depositor: lt.address, routeType: opts.route === "1" ? 1 : 0 });
|
|
747
|
+
const view = {
|
|
748
|
+
venue: rhDef.id, network: net, asset: plan.asset, amount: plan.amount, depositor: plan.depositor,
|
|
749
|
+
contract: plan.contract, token: plan.token, chainId: plan.chainId, routeType: plan.routeType,
|
|
750
|
+
needsApprove: plan.needsApprove,
|
|
751
|
+
};
|
|
752
|
+
if (isDryRun()) {
|
|
753
|
+
logExecution({
|
|
754
|
+
type: "bridge", exchange: rhDef.id, symbol: plan.asset, side: "deposit",
|
|
755
|
+
size: plan.amount, status: "simulated", dryRun: true,
|
|
756
|
+
meta: { action: "deposit", method: "contract", contract: plan.contract },
|
|
757
|
+
});
|
|
758
|
+
if (isJson())
|
|
759
|
+
return printJson(jsonOk({ dryRun: true, sent: false, ...view }));
|
|
760
|
+
console.log(chalk.yellow(`\n [dry-run] Would deposit ${plan.amount} ${plan.asset} into ${rhDef.displayName}`));
|
|
761
|
+
console.log(` From: ${plan.depositor} (${rhChain}, chainId ${plan.chainId})`);
|
|
762
|
+
console.log(` Contract: ${plan.contract}`);
|
|
763
|
+
console.log(` Steps: ${plan.needsApprove ? `approve ${plan.asset} → ` : ""}deposit(route ${plan.routeType})`);
|
|
764
|
+
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (!isJson()) {
|
|
768
|
+
console.log(chalk.cyan(`\n Depositing ${plan.amount} ${plan.asset} into ${rhDef.displayName} on ${rhChain}...\n`));
|
|
769
|
+
if (plan.needsApprove)
|
|
770
|
+
console.log(chalk.gray(` Approving ${plan.asset} for ${plan.contract}...`));
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const res = await executeLighterRhDeposit(plan, tx => lt.signL1Transaction(tx));
|
|
774
|
+
logExecution({
|
|
775
|
+
type: "bridge", exchange: rhDef.id, symbol: plan.asset, side: "deposit",
|
|
776
|
+
size: plan.amount, status: "success", dryRun: false,
|
|
777
|
+
meta: { action: "deposit", method: "contract", txHash: res.depositTxHash, approveTxHash: res.approveTxHash },
|
|
778
|
+
});
|
|
779
|
+
if (isJson())
|
|
780
|
+
return printJson(jsonOk({ ...view, ...res, method: "contract" }));
|
|
781
|
+
console.log(chalk.green(`\n Deposit confirmed!`));
|
|
782
|
+
console.log(` Amount: ${plan.amount} ${plan.asset}`);
|
|
783
|
+
if (res.approveTxHash !== null)
|
|
784
|
+
console.log(` Approve: ${res.approveTxHash}`);
|
|
785
|
+
console.log(` TX Hash: ${res.depositTxHash}`);
|
|
786
|
+
console.log(chalk.gray(`\n Funds appear in your ${rhDef.displayName} account shortly.\n`));
|
|
787
|
+
}
|
|
788
|
+
catch (err) {
|
|
789
|
+
logExecution({
|
|
790
|
+
type: "bridge", exchange: rhDef.id, symbol: plan.asset, side: "deposit",
|
|
791
|
+
size: plan.amount, status: fundsFailStatus(err), dryRun: false,
|
|
792
|
+
error: err instanceof Error ? err.message : String(err),
|
|
793
|
+
// An unknown-outcome (ORDER_PENDING) failure carries the broadcast tx hash: keep it in the log.
|
|
794
|
+
meta: { action: "deposit", method: "contract", ...pendingTxMeta(err) },
|
|
795
|
+
});
|
|
796
|
+
throw err;
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
// ═══════════════════════════════════════════════════════
|
|
801
|
+
// FAUCET (testnet tooling)
|
|
802
|
+
// ═══════════════════════════════════════════════════════
|
|
803
|
+
const faucet = funds.command("faucet").description("[testnet tooling] Mint venue test collateral (refused on mainnet)");
|
|
804
|
+
supportsDryRun(faucet.command("pacifica <amount>"))
|
|
805
|
+
.description("[testnet tooling] Mint test USDP on Pacifica testnet (Solana devnet mint_test_usdc; master wallet; simulated before send; supports --dry-run)")
|
|
806
|
+
.action(async (amount) => {
|
|
807
|
+
const network = getNetwork();
|
|
808
|
+
if (network !== "testnet") {
|
|
809
|
+
throw unsupported("Pacifica", `the test-USDP faucet on ${network}`, "mint_test_usdc exists only on the devnet program", "Run on testnet: perp -n testnet funds faucet pacifica <amount>");
|
|
810
|
+
}
|
|
811
|
+
const { testUsdcBaseUnits, mintTestUsdc, PACIFICA_DEVNET_RPC } = await import("../pacifica/faucet.js");
|
|
812
|
+
const { getNetworkConfig } = await import("../pacifica/constants.js");
|
|
813
|
+
const baseUnits = testUsdcBaseUnits(amount);
|
|
814
|
+
const cfg = getNetworkConfig("testnet");
|
|
815
|
+
if (isDryRun()) {
|
|
816
|
+
return printFundsPlan({
|
|
817
|
+
action: "deposit", venue: "pacifica", network, asset: "USDP (test)", amount,
|
|
818
|
+
method: "faucet mint_test_usdc (simulated, then sent)", baseUnits: baseUnits.toString(),
|
|
819
|
+
programId: cfg.programId, mint: cfg.usdcMint, rpc: PACIFICA_DEVNET_RPC,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
await requireMaster("pacifica", "Pacifica testnet faucet");
|
|
823
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
824
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
825
|
+
if (adapter.name !== "pacifica")
|
|
826
|
+
throw new Error("pacifica venue did not produce a Pacifica adapter");
|
|
827
|
+
const { PacificaAdapter } = await import("../exchanges/pacifica.js");
|
|
828
|
+
const pac = adapter;
|
|
829
|
+
const { Connection } = await import("@solana/web3.js");
|
|
830
|
+
try {
|
|
831
|
+
const { signature, plan } = await mintTestUsdc(new Connection(PACIFICA_DEVNET_RPC, "confirmed"), pac.masterSolanaSigner(), amount);
|
|
832
|
+
logExecution({
|
|
833
|
+
type: "bridge", exchange: "pacifica", symbol: "USDP", side: "deposit",
|
|
834
|
+
size: amount, status: "success", dryRun: false,
|
|
835
|
+
meta: { action: "faucet", network, signature, userAta: plan.userAta },
|
|
836
|
+
});
|
|
837
|
+
if (isJson())
|
|
838
|
+
return printJson(jsonOk({ signature, amount, network, method: "faucet", wallet: plan.user, userAta: plan.userAta }));
|
|
839
|
+
console.log(chalk.green(`\n Minted ${amount} test USDP on Pacifica testnet`));
|
|
840
|
+
console.log(` Wallet: ${plan.user}`);
|
|
841
|
+
console.log(` Signature: ${signature}\n`);
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
logExecution({
|
|
845
|
+
type: "bridge", exchange: "pacifica", symbol: "USDP", side: "deposit",
|
|
846
|
+
size: amount, status: fundsFailStatus(err), dryRun: false,
|
|
847
|
+
error: err instanceof Error ? err.message : String(err),
|
|
848
|
+
meta: { action: "faucet", network },
|
|
849
|
+
});
|
|
850
|
+
throw err;
|
|
851
|
+
}
|
|
852
|
+
});
|
|
515
853
|
// ═══════════════════════════════════════════════════════
|
|
516
854
|
// WITHDRAW
|
|
517
855
|
// ═══════════════════════════════════════════════════════
|
|
518
856
|
const withdraw = funds.command("withdraw").description("Withdraw funds from exchange accounts");
|
|
519
857
|
// ── Pacifica ──
|
|
520
|
-
withdraw
|
|
521
|
-
.
|
|
522
|
-
.
|
|
523
|
-
|
|
524
|
-
.action(async (amount, opts) => {
|
|
525
|
-
const amountNum = parseFloat(amount);
|
|
858
|
+
supportsDryRun(withdraw.command("pacifica <amount>"))
|
|
859
|
+
.description("Withdraw USDC from Pacifica to your own Solana wallet (Pacifica has no destination field; supports --dry-run)")
|
|
860
|
+
.action(async (amount) => {
|
|
861
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
526
862
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
527
863
|
throw new Error("Invalid amount");
|
|
528
|
-
|
|
864
|
+
if (isDryRun()) {
|
|
865
|
+
return printFundsPlan({ action: "withdraw", venue: "pacifica", network: getNetwork(), asset: "USDC", amount: String(amountNum), destination: "account owner's Solana wallet" });
|
|
866
|
+
}
|
|
867
|
+
await requireMaster("pacifica", "Pacifica withdraw");
|
|
868
|
+
// The subcommand names the venue: fetch it directly, independent of -e.
|
|
869
|
+
const adapter = await getAdapterForExchange("pacifica");
|
|
529
870
|
if (!hasPacificaSdk(adapter) || !isWithdrawCapable(adapter))
|
|
530
|
-
throw new Error("
|
|
531
|
-
|
|
871
|
+
throw new Error("pacifica venue did not produce a withdraw-capable Pacifica adapter");
|
|
872
|
+
// Pacifica withdrawals always go to the account owner's own wallet.
|
|
873
|
+
const dest = adapter.publicKey;
|
|
532
874
|
if (!isJson()) {
|
|
533
875
|
console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Pacifica...\n`));
|
|
534
876
|
console.log(` Destination: ${dest}`);
|
|
@@ -549,7 +891,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
549
891
|
catch (err) {
|
|
550
892
|
logExecution({
|
|
551
893
|
type: "bridge", exchange: "pacifica", symbol: "USDC", side: "withdraw",
|
|
552
|
-
size: String(amountNum), status:
|
|
894
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
553
895
|
error: err instanceof Error ? err.message : String(err),
|
|
554
896
|
meta: { action: "withdraw", destination: dest },
|
|
555
897
|
});
|
|
@@ -557,21 +899,52 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
557
899
|
}
|
|
558
900
|
});
|
|
559
901
|
// ── Hyperliquid ──
|
|
560
|
-
withdraw
|
|
561
|
-
.
|
|
562
|
-
.
|
|
563
|
-
.option("--to <address>", "Destination EVM address (default: your wallet)")
|
|
902
|
+
supportsDryRun(withdraw.command("hyperliquid <amount>"))
|
|
903
|
+
.description("Withdraw USDC from Hyperliquid over CCTP (native USDC) to your wallet on Arbitrum (default), Ethereum or Base — another address only when allowlisted in settings withdraw.hyperliquid; supports --dry-run")
|
|
904
|
+
.option("--chain <chain>", "Destination chain: arbitrum | ethereum | base", "arbitrum")
|
|
905
|
+
.option("--to <address>", "Destination EVM address (default: your wallet; any other address must be on the settings withdraw.hyperliquid allowlist)")
|
|
564
906
|
.action(async (amount, opts) => {
|
|
565
|
-
const
|
|
907
|
+
const { HL_CCTP_ROUTES } = await import("../exchanges/hyperliquid.js");
|
|
908
|
+
const chain = opts.chain.toLowerCase();
|
|
909
|
+
if (!(chain in HL_CCTP_ROUTES)) {
|
|
910
|
+
throw new PerpError("INVALID_PARAMS", `--chain must be one of ${Object.keys(HL_CCTP_ROUTES).join(", ")} (got "${opts.chain}")`);
|
|
911
|
+
}
|
|
912
|
+
const route = HL_CCTP_ROUTES[chain];
|
|
913
|
+
const feeNote = route.forwardingFeeUsdc !== null
|
|
914
|
+
? `CCTP forwarding fee ${route.forwardingFeeUsdc} USDC (deducted on the way) + a small Hyperliquid send fee`
|
|
915
|
+
: "CCTP forwarding fee for this chain is not published — it is deducted on the way";
|
|
916
|
+
const amountStr = parsePositiveDecimal(amount, "amount");
|
|
917
|
+
const amountNum = Number(amountStr);
|
|
566
918
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
567
919
|
throw new Error("Invalid amount");
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
920
|
+
if (opts.to !== undefined && !/^0x[0-9a-fA-F]{40}$/.test(opts.to)) {
|
|
921
|
+
throw new PerpError("INVALID_PARAMS", `--to must be an EVM address (got "${opts.to}")`);
|
|
922
|
+
}
|
|
923
|
+
if (isDryRun()) {
|
|
924
|
+
// The owner address is not resolved in a dry run (no key is loaded): an explicit --to is
|
|
925
|
+
// judged as a non-owner destination; it needs no gate if it is the owner's own address.
|
|
926
|
+
const withdrawGate = opts.to === undefined
|
|
927
|
+
? "not needed — destination is the account owner's own address"
|
|
928
|
+
: `${hyperliquidGateVerdict(getNetwork(), amountStr, opts.to, Date.now())} (not needed if --to is the account owner's own address; checked again when sent)`;
|
|
929
|
+
return printFundsPlan({ action: "withdraw", venue: "hyperliquid", network: getNetwork(), asset: "USDC", amount: String(amountNum), destination: opts.to !== undefined ? opts.to : "account owner's address", chain, method: "CCTP (sendToEvmWithData)", cctpDomain: route.cctpDomain, fee: feeNote, withdrawGate });
|
|
930
|
+
}
|
|
931
|
+
await requireMaster("hyperliquid", "Hyperliquid withdraw");
|
|
932
|
+
// The subcommand names the venue: fetch it directly, independent of -e (like ondo / arcus).
|
|
933
|
+
const adapter = await getAdapterForExchange("hyperliquid");
|
|
934
|
+
if (!hasEvmAddress(adapter) || !isWithdrawCapable(adapter))
|
|
935
|
+
throw new Error("hyperliquid venue did not produce a withdraw-capable EVM adapter");
|
|
936
|
+
// HyperliquidAdapter.withdraw takes the CCTP destination chain.
|
|
937
|
+
const hlWithdraw = adapter.withdraw.bind(adapter);
|
|
938
|
+
const dest = opts.to === undefined ? adapter.address : opts.to;
|
|
939
|
+
// Owner-only unless the destination passes settings.withdraw.hyperliquid (checked before any read / send).
|
|
940
|
+
const toOwner = dest.toLowerCase() === adapter.address.toLowerCase();
|
|
941
|
+
if (!toOwner) {
|
|
942
|
+
const gate = loadSettings().withdraw.hyperliquid;
|
|
943
|
+
assertWithdrawAllowed("hyperliquid", getNetwork(), amountStr, dest, gate === undefined ? null : gate, Date.now());
|
|
944
|
+
}
|
|
572
945
|
if (!isJson()) {
|
|
573
946
|
console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Hyperliquid...\n`));
|
|
574
|
-
console.log(` Destination: ${dest}`);
|
|
947
|
+
console.log(` Destination: ${dest}${toOwner ? "" : " (allowlisted in settings withdraw.hyperliquid)"}`);
|
|
575
948
|
}
|
|
576
949
|
const balance = await adapter.getBalance();
|
|
577
950
|
const available = Number(balance.available);
|
|
@@ -584,23 +957,27 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
584
957
|
}
|
|
585
958
|
process.exit(1);
|
|
586
959
|
}
|
|
960
|
+
const attempt = hyperliquidGateEnforce(getNetwork(), amountStr, dest, adapter.address, Date.now());
|
|
587
961
|
try {
|
|
588
|
-
const result = await
|
|
962
|
+
const result = await hlWithdraw(amountStr, dest, { chain: chain });
|
|
589
963
|
logExecution({
|
|
590
964
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "withdraw",
|
|
591
965
|
size: String(amountNum), status: "success", dryRun: false,
|
|
592
|
-
meta: { action: "withdraw", destination: dest },
|
|
966
|
+
meta: { action: "withdraw", destination: dest, chain, method: "cctp" },
|
|
593
967
|
});
|
|
594
968
|
if (isJson())
|
|
595
|
-
return printJson(jsonOk(result));
|
|
596
|
-
console.log(chalk.green(`\n Withdrawal submitted!`));
|
|
597
|
-
console.log(` Amount: $${formatUsd(amountNum)} USDC`);
|
|
969
|
+
return printJson(jsonOk({ result, chain, method: "cctp", fee: feeNote }));
|
|
970
|
+
console.log(chalk.green(`\n Withdrawal submitted (CCTP → ${chain})!`));
|
|
971
|
+
console.log(` Amount: $${formatUsd(amountNum)} USDC — ${feeNote}`);
|
|
598
972
|
console.log(chalk.gray(`\n Processing may take a few minutes.\n`));
|
|
599
973
|
}
|
|
600
974
|
catch (err) {
|
|
975
|
+
// Only a definite refusal proves nothing left the account; an unknown outcome keeps counting.
|
|
976
|
+
if (attempt !== null && isDefiniteOrderFailure(err))
|
|
977
|
+
forgetWithdrawAttempt(attempt);
|
|
601
978
|
logExecution({
|
|
602
979
|
type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "withdraw",
|
|
603
|
-
size: String(amountNum), status:
|
|
980
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
604
981
|
error: err instanceof Error ? err.message : String(err),
|
|
605
982
|
meta: { action: "withdraw", destination: dest },
|
|
606
983
|
});
|
|
@@ -608,62 +985,338 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
608
985
|
}
|
|
609
986
|
});
|
|
610
987
|
// ── Lighter ──
|
|
611
|
-
withdraw
|
|
612
|
-
|
|
613
|
-
.description(
|
|
614
|
-
.option("--asset-id <id>", "Asset
|
|
988
|
+
supportsDryRun(withdraw.command("lighter <amount>"))
|
|
989
|
+
// Serves both Lighter-family venues (-e lighter / -e lighter-rh); text derived from their definitions.
|
|
990
|
+
.description(`Withdraw collateral from a Lighter-family venue to your own L1 wallet (${lighterFamilyWithdrawSummary()}; supports --dry-run)`)
|
|
991
|
+
.option("--asset-id <id>", "Asset index (default: 3 = the venue collateral, per /api/v1/assetDetails)", "3")
|
|
615
992
|
.option("--route <type>", "Route type: 0=perp, 1=spot (default: 0)", "0")
|
|
616
993
|
.action(async (amount, opts) => {
|
|
617
|
-
const amountNum =
|
|
994
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
618
995
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
619
996
|
throw new Error("Invalid amount");
|
|
997
|
+
if (isDryRun()) {
|
|
998
|
+
const venue = getVenue(program.opts().exchange);
|
|
999
|
+
if (venue.family !== "lighter")
|
|
1000
|
+
throw new Error("Requires --exchange lighter or lighter-rh");
|
|
1001
|
+
return printFundsPlan({ action: "withdraw", venue: venue.id, network: getNetwork(), asset: withdrawAssetOf(venue), amount: String(amountNum), destination: "account owner's L1 wallet", chain: withdrawChainOf(venue, getNetwork()), assetIndex: opts.assetId, routeType: opts.route });
|
|
1002
|
+
}
|
|
1003
|
+
{
|
|
1004
|
+
const venue = getVenue(program.opts().exchange);
|
|
1005
|
+
if (venue.family === "lighter")
|
|
1006
|
+
await requireMaster(venue.id, `${venue.displayName} withdraw`);
|
|
1007
|
+
}
|
|
620
1008
|
const adapter = await getAdapter();
|
|
621
1009
|
if (!hasLighterAccount(adapter) || !isWithdrawCapable(adapter))
|
|
622
|
-
throw new Error("Requires --exchange lighter");
|
|
1010
|
+
throw new Error("Requires --exchange lighter or lighter-rh");
|
|
1011
|
+
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
1012
|
+
if (!(adapter instanceof LighterAdapter))
|
|
1013
|
+
throw new Error("Requires --exchange lighter or lighter-rh");
|
|
1014
|
+
const venueName = getVenue(adapter.venueId).displayName;
|
|
1015
|
+
const asset = adapter.collateralSymbol;
|
|
1016
|
+
const chain = chainLabel(adapter.network.settlementChain);
|
|
623
1017
|
if (!isJson()) {
|
|
624
|
-
console.log(chalk.cyan(`\n Withdrawing
|
|
1018
|
+
console.log(chalk.cyan(`\n Withdrawing ${formatUsd(amountNum)} ${asset} from ${venueName} to ${chain}...\n`));
|
|
625
1019
|
console.log(` Account Index: ${adapter.accountIndex}`);
|
|
626
1020
|
console.log(` Address: ${adapter.address}`);
|
|
627
1021
|
}
|
|
628
1022
|
try {
|
|
629
1023
|
const result = await adapter.withdraw(String(amountNum), "", { assetId: parseInt(opts.assetId), routeType: parseInt(opts.route) });
|
|
630
1024
|
logExecution({
|
|
631
|
-
type: "bridge", exchange:
|
|
1025
|
+
type: "bridge", exchange: adapter.venueId, symbol: asset, side: "withdraw",
|
|
632
1026
|
size: String(amountNum), status: "success", dryRun: false,
|
|
633
1027
|
meta: { action: "withdraw" },
|
|
634
1028
|
});
|
|
635
1029
|
if (isJson())
|
|
636
1030
|
return printJson(jsonOk(result));
|
|
637
1031
|
console.log(chalk.green(`\n Withdrawal submitted!`));
|
|
638
|
-
console.log(` Amount:
|
|
639
|
-
console.log(chalk.gray(`\n Standard withdrawal
|
|
1032
|
+
console.log(` Amount: ${formatUsd(amountNum)} ${asset}`);
|
|
1033
|
+
console.log(chalk.gray(`\n Standard (secure) withdrawal is processed by the ${chain} contract and can take hours. Use the ${venueName} web app for a fast withdrawal.\n`));
|
|
640
1034
|
}
|
|
641
1035
|
catch (err) {
|
|
642
1036
|
logExecution({
|
|
643
|
-
type: "bridge", exchange:
|
|
644
|
-
size: String(amountNum), status:
|
|
1037
|
+
type: "bridge", exchange: adapter.venueId, symbol: asset, side: "withdraw",
|
|
1038
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
645
1039
|
error: err instanceof Error ? err.message : String(err),
|
|
646
1040
|
meta: { action: "withdraw" },
|
|
647
1041
|
});
|
|
648
1042
|
throw err;
|
|
649
1043
|
}
|
|
650
1044
|
});
|
|
1045
|
+
// ── Arcus — uniform surface (design §13.1): same <amount> + --to,
|
|
1046
|
+
// same jsonOk(result), global --dry-run plans without sending. ──
|
|
1047
|
+
// Ondo Perps: the API does not allow withdrawals (address-book destinations only; adding one is
|
|
1048
|
+
// refused for API keys). The command stays so the refusal is explicit — also under --dry-run.
|
|
1049
|
+
supportsDryRun(withdraw.command("ondo [amount]"))
|
|
1050
|
+
.description("Not available: Ondo Perps withdrawals are web-app only (refused, also with --dry-run)")
|
|
1051
|
+
.option("--to <address>", "Ignored — the API cannot withdraw")
|
|
1052
|
+
.action(async () => {
|
|
1053
|
+
throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
|
|
1054
|
+
});
|
|
1055
|
+
for (const venueId of ["arcus"]) {
|
|
1056
|
+
const def = getVenue(venueId);
|
|
1057
|
+
const asset = withdrawAssetOf(def);
|
|
1058
|
+
const toHelp = "Destination EVM address (must equal the Arcus account owner — the default)";
|
|
1059
|
+
supportsDryRun(withdraw.command(`${venueId} <amount>`))
|
|
1060
|
+
.description(`Withdraw ${asset} from ${def.displayName} to your ${chainLabel(withdrawChainOf(def, "mainnet"))} wallet (supports --dry-run)`)
|
|
1061
|
+
.option("--to <address>", toHelp)
|
|
1062
|
+
.action(async (amount, opts) => {
|
|
1063
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
1064
|
+
if (!/^\d+(\.\d+)?$/.test(amount) || isNaN(amountNum) || amountNum <= 0) {
|
|
1065
|
+
throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
|
|
1066
|
+
}
|
|
1067
|
+
const net = getNetwork();
|
|
1068
|
+
const chain = chainLabel(withdrawChainOf(def, net));
|
|
1069
|
+
const adapter = await getAdapterForExchange(venueId);
|
|
1070
|
+
if (!isWithdrawCapable(adapter))
|
|
1071
|
+
throw new Error(`${def.id} adapter has no withdraw()`);
|
|
1072
|
+
const { ArcusAdapter } = await import("../exchanges/arcus/adapter.js");
|
|
1073
|
+
if (!(adapter instanceof ArcusAdapter))
|
|
1074
|
+
throw new Error("arcus venue did not produce an ArcusAdapter");
|
|
1075
|
+
const owner = adapter.withdrawDestination;
|
|
1076
|
+
if (opts.to !== undefined && opts.to.toLowerCase() !== owner.toLowerCase()) {
|
|
1077
|
+
throw new PerpError("INVALID_PARAMS", `Arcus only withdraws to the account owner ${owner}`, {
|
|
1078
|
+
remediation: `Omit --to (defaults to ${owner})`,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
const destination = owner;
|
|
1082
|
+
const plan = { venue: def.id, network: net, asset, amount: String(amountNum), destination, chain };
|
|
1083
|
+
if (isDryRun()) {
|
|
1084
|
+
logExecution({
|
|
1085
|
+
type: "bridge", exchange: def.id, symbol: asset, side: "withdraw",
|
|
1086
|
+
size: String(amountNum), status: "simulated", dryRun: true,
|
|
1087
|
+
meta: { action: "withdraw", destination },
|
|
1088
|
+
});
|
|
1089
|
+
if (isJson())
|
|
1090
|
+
return printJson(jsonOk({ dryRun: true, sent: false, ...plan }));
|
|
1091
|
+
console.log(chalk.yellow(`\n [dry-run] Would withdraw ${formatUsd(amountNum)} ${asset} from ${def.displayName} (${net})`));
|
|
1092
|
+
console.log(` Destination: ${destination} (${chain})`);
|
|
1093
|
+
console.log(chalk.gray(" Nothing was sent.\n"));
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (!isJson()) {
|
|
1097
|
+
console.log(chalk.cyan(`\n Withdrawing ${formatUsd(amountNum)} ${asset} from ${def.displayName}...\n`));
|
|
1098
|
+
console.log(` Destination: ${destination} (${chain})`);
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
const result = await adapter.withdraw(String(amountNum), destination);
|
|
1102
|
+
logExecution({
|
|
1103
|
+
type: "bridge", exchange: def.id, symbol: asset, side: "withdraw",
|
|
1104
|
+
size: String(amountNum), status: "success", dryRun: false,
|
|
1105
|
+
meta: { action: "withdraw", destination },
|
|
1106
|
+
});
|
|
1107
|
+
if (isJson())
|
|
1108
|
+
return printJson(jsonOk(result));
|
|
1109
|
+
console.log(chalk.green(`\n Withdrawal submitted!`));
|
|
1110
|
+
console.log(` Amount: ${formatUsd(amountNum)} ${asset}`);
|
|
1111
|
+
console.log(chalk.gray(`\n Track it in the ${def.displayName} app until it arrives on ${chain}.\n`));
|
|
1112
|
+
}
|
|
1113
|
+
catch (err) {
|
|
1114
|
+
logExecution({
|
|
1115
|
+
type: "bridge", exchange: def.id, symbol: asset, side: "withdraw",
|
|
1116
|
+
size: String(amountNum), status: fundsFailStatus(err), dryRun: false,
|
|
1117
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1118
|
+
meta: { action: "withdraw", destination },
|
|
1119
|
+
});
|
|
1120
|
+
throw err;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
// ── QFEX (design §14.6, §14.12 #8) — custodial; deposit = address display, withdraw = banker POST
|
|
1125
|
+
// gated by settings.withdraw.qfex inside QfexAdapter.withdraw(); switched off (QFEX_WITHDRAW_ENABLED). ──
|
|
1126
|
+
/** QFEX adapter for the active network — the default imported key, or the one named by --agent. */
|
|
1127
|
+
const qfexAdapterFor = async (agentName) => {
|
|
1128
|
+
const { QfexAdapter: Cls } = await import("../exchanges/qfex/adapter.js");
|
|
1129
|
+
let adapter;
|
|
1130
|
+
if (agentName === undefined) {
|
|
1131
|
+
adapter = await getAdapterForExchange("qfex");
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
const { getVenueAgent } = await import("../agent-wallet/store.js");
|
|
1135
|
+
const meta = getVenueAgent("qfex", agentName);
|
|
1136
|
+
if (meta === null) {
|
|
1137
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `No QFEX API key named "${agentName}"`, { remediation: "List keys: perp wallet agent list qfex" });
|
|
1138
|
+
}
|
|
1139
|
+
const def = getVenue("qfex");
|
|
1140
|
+
if (def.create === null)
|
|
1141
|
+
throw new Error("qfex venue has no adapter factory");
|
|
1142
|
+
adapter = await def.create({
|
|
1143
|
+
network: getNetwork(), credentials: { ...READ_ONLY_CREDENTIALS, agent: meta }, noAgent: false, hlDex: null, readOnly: false, noBuilder: true,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
if (!(adapter instanceof Cls))
|
|
1147
|
+
throw new Error("qfex venue did not produce a QfexAdapter");
|
|
1148
|
+
return adapter;
|
|
1149
|
+
};
|
|
1150
|
+
supportsDryRun(withdraw.command(QFEX_WITHDRAW_ENABLED ? "qfex <amount>" : "qfex [amount]"))
|
|
1151
|
+
.description(QFEX_WITHDRAW_ENABLED
|
|
1152
|
+
? "Withdraw USDC from QFEX to an allowlisted Arbitrum One address (needs settings withdraw.qfex and a key with the \"Deposit and withdraw\" permission; supports --dry-run)"
|
|
1153
|
+
: "Not available: QFEX withdrawals are disabled in perp-cli for now — use the QFEX web app (refused, also with --dry-run)")
|
|
1154
|
+
.option("--to <address>", "Destination EVM address on Arbitrum One (required; must be on the settings withdraw.qfex allowlist)")
|
|
1155
|
+
.option("--agent <name>", "Imported QFEX key to use (a key with the \"Deposit and withdraw\" permission, bound to the primary account)")
|
|
1156
|
+
.action(async (amount, opts) => {
|
|
1157
|
+
if (!QFEX_WITHDRAW_ENABLED)
|
|
1158
|
+
throw unsupported("QFEX", "withdraw", QFEX_WITHDRAW_DISABLED.reason, QFEX_WITHDRAW_DISABLED.remediation);
|
|
1159
|
+
const def = getVenue("qfex");
|
|
1160
|
+
const net = getNetwork();
|
|
1161
|
+
if (!/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0)) {
|
|
1162
|
+
throw new PerpError("INVALID_PARAMS", `Withdraw amount must be a positive decimal (got "${amount}")`);
|
|
1163
|
+
}
|
|
1164
|
+
if (opts.to === undefined) {
|
|
1165
|
+
throw new PerpError("INVALID_PARAMS", "QFEX withdraw needs a destination address (the API key has no owner address)", {
|
|
1166
|
+
remediation: `perp funds withdraw qfex ${amount} --to <0x address on the settings withdraw.qfex allowlist>`,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
const destination = opts.to;
|
|
1170
|
+
const chain = chainLabel(withdrawChainOf(def, net));
|
|
1171
|
+
if (isDryRun()) {
|
|
1172
|
+
// The local gate needs no key and no network: a dry run shows whether it would refuse.
|
|
1173
|
+
const gate = loadSettings().withdraw.qfex;
|
|
1174
|
+
assertWithdrawAllowed("qfex", net, amount, destination, gate === undefined ? null : gate, Date.now());
|
|
1175
|
+
return printFundsPlan({
|
|
1176
|
+
action: "withdraw", venue: "qfex", network: net, asset: "USDC", amount, destination, chain, bankerNetwork: QFEX_BANKER_NETWORK,
|
|
1177
|
+
fee: "taken from the amount by QFEX (not published)", // docs.qfex.com/api-reference/programmatic-funding: "`POST /withdraw` is not idempotent" and 403 =
|
|
1178
|
+
// "API key lacks **Deposit and withdraw** permission, or the account cannot use funding".
|
|
1179
|
+
// 2FA is documented for creating the API key (api-reference/api-keys), not for the withdrawal call.
|
|
1180
|
+
note: "POST /withdraw is not idempotent; QFEX answers 403 when the key lacks the \"Deposit and withdraw\" permission or the account cannot use funding",
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
const adapter = await qfexAdapterFor(opts.agent);
|
|
1184
|
+
if (!isJson()) {
|
|
1185
|
+
console.log(chalk.cyan(`\n Withdrawing ${amount} USDC from QFEX...\n`));
|
|
1186
|
+
console.log(` Destination: ${destination} (${chain})`);
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
const result = await adapter.withdraw(amount, destination);
|
|
1190
|
+
logExecution({ type: "bridge", exchange: "qfex", symbol: "USDC", side: "withdraw", size: amount, status: "success", dryRun: false, meta: { action: "withdraw", destination } });
|
|
1191
|
+
if (isJson())
|
|
1192
|
+
return printJson(jsonOk(result));
|
|
1193
|
+
console.log(chalk.green(`\n Withdrawal accepted by QFEX.`));
|
|
1194
|
+
console.log(` Amount: ${amount} USDC (fee taken from it; not published)`);
|
|
1195
|
+
console.log(chalk.gray(`\n ${result.note}\n`));
|
|
1196
|
+
}
|
|
1197
|
+
catch (err) {
|
|
1198
|
+
logExecution({ type: "bridge", exchange: "qfex", symbol: "USDC", side: "withdraw", size: amount, status: fundsFailStatus(err), dryRun: false, error: err instanceof Error ? err.message : String(err), meta: { action: "withdraw", destination } });
|
|
1199
|
+
throw err;
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
supportsDryRun(deposit.command("qfex"))
|
|
1203
|
+
.description("Show the QFEX deposit address for USDC on Arbitrum One (perp-cli sends nothing — transfer from your own wallet; supports --dry-run)")
|
|
1204
|
+
// --chain, not --network: the root -n/--network (mainnet | testnet) would swallow a subcommand --network.
|
|
1205
|
+
.option("--chain <chain>", "Deposit chain (arbitrum only; TRON USDT is not supported)", "arbitrum")
|
|
1206
|
+
.option("--agent <name>", "Imported QFEX key to use (needs the \"Deposit and withdraw\" permission)")
|
|
1207
|
+
.action(async (opts) => {
|
|
1208
|
+
const requested = opts.chain.toLowerCase();
|
|
1209
|
+
if (requested === "tron") {
|
|
1210
|
+
throw unsupported("qfex", "depositTron", "only USDC on Arbitrum One is supported; how a TRON USDT deposit is credited as USDC margin is not documented", "Deposit USDC on Arbitrum One, or use the QFEX web app for other rails");
|
|
1211
|
+
}
|
|
1212
|
+
if (requested !== QFEX_WITHDRAW_NETWORK) {
|
|
1213
|
+
throw new PerpError("INVALID_PARAMS", `Unsupported QFEX deposit chain "${opts.chain}"`, { remediation: "Use --chain arbitrum" });
|
|
1214
|
+
}
|
|
1215
|
+
const net = getNetwork();
|
|
1216
|
+
if (isDryRun()) {
|
|
1217
|
+
return printFundsPlan({
|
|
1218
|
+
action: "deposit", venue: "qfex", network: net, asset: "USDC", amount: "(you choose)", chain: chainLabel(QFEX_WITHDRAW_NETWORK),
|
|
1219
|
+
method: `GET ${QFEX_BANKER_NETWORK} deposit address from the QFEX banker, then YOU send USDC to it from your wallet`,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
const adapter = await qfexAdapterFor(opts.agent);
|
|
1223
|
+
const addr = await adapter.depositAddress();
|
|
1224
|
+
const info = {
|
|
1225
|
+
venue: "qfex", network: net, address: addr.address, depositNetwork: addr.network, asset: addr.asset,
|
|
1226
|
+
minimum: null, confirmations: null,
|
|
1227
|
+
warning: "Send ONLY USDC on Arbitrum One to this address. Any other asset or network is lost. Minimum amount and confirmations: see the QFEX funding screen. Deposits credit the primary account.",
|
|
1228
|
+
};
|
|
1229
|
+
if (isJson())
|
|
1230
|
+
return printJson(jsonOk(info));
|
|
1231
|
+
console.log(chalk.cyan.bold(`\n QFEX deposit address (${net})\n`));
|
|
1232
|
+
console.log(` Address: ${chalk.green(addr.address)}`);
|
|
1233
|
+
console.log(` Network: ${addr.network} (Arbitrum One) Asset: ${addr.asset}`);
|
|
1234
|
+
console.log(chalk.yellow(`\n ${info.warning}`));
|
|
1235
|
+
console.log(chalk.gray(" perp-cli did not send anything — transfer from your own wallet or exchange.\n"));
|
|
1236
|
+
});
|
|
1237
|
+
// ── Aster deposit / withdraw, Ondo deposit (OWS-master on-chain legs; address book refused) ──
|
|
1238
|
+
registerOnchainFundsCommands({
|
|
1239
|
+
program, funds, deposit, withdraw, isJson, isDryRun, getNetwork, getAdapterForExchange,
|
|
1240
|
+
requireMaster, printFundsPlan, failStatus: fundsFailStatus,
|
|
1241
|
+
});
|
|
651
1242
|
// ═══════════════════════════════════════════════════════
|
|
652
1243
|
// TRANSFER (HL internal)
|
|
653
1244
|
// ═══════════════════════════════════════════════════════
|
|
654
|
-
funds
|
|
655
|
-
.
|
|
656
|
-
.
|
|
657
|
-
.
|
|
658
|
-
|
|
1245
|
+
supportsDryRun(funds.command("transfer <amount> <destination>"))
|
|
1246
|
+
.description("Transfer USDC to another Hyperliquid account (internal, instant; the destination must be on the settings withdraw.hyperliquid allowlist), or — with -e qfex — between your QFEX accounts (destination: primary | <account uuid>; supports --dry-run)")
|
|
1247
|
+
.option("--from <account>", "QFEX only: source account (primary | <account uuid>; default: the account the key is bound to)")
|
|
1248
|
+
.option("--agent <name>", "QFEX only: imported key to use")
|
|
1249
|
+
.action(async (amount, destination, opts) => {
|
|
1250
|
+
const amountNum = Number(parsePositiveDecimal(amount, "amount"));
|
|
659
1251
|
if (isNaN(amountNum) || amountNum <= 0)
|
|
660
1252
|
throw new Error("Invalid amount");
|
|
661
|
-
const
|
|
1253
|
+
const exchangeOpt = program.opts().exchange;
|
|
1254
|
+
if (exchangeOpt !== undefined && getVenue(exchangeOpt).id === "qfex") {
|
|
1255
|
+
if (isDryRun()) {
|
|
1256
|
+
return printFundsPlan({ action: "transfer", venue: "qfex", network: getNetwork(), asset: "USDC", amount, from: opts.from ?? "the key's account", destination });
|
|
1257
|
+
}
|
|
1258
|
+
const adapter = await qfexAdapterFor(opts.agent);
|
|
1259
|
+
// "primary" is resolved from the venue's own account list (is_master) — never assumed.
|
|
1260
|
+
const accounts = await adapter.listAccounts();
|
|
1261
|
+
const resolve = (name) => {
|
|
1262
|
+
if (name.toLowerCase() !== "primary")
|
|
1263
|
+
return name.toLowerCase();
|
|
1264
|
+
const primary = accounts.filter(a => a.isPrimary);
|
|
1265
|
+
if (primary.length !== 1)
|
|
1266
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX lists ${primary.length} primary accounts`, { exchange: "qfex" });
|
|
1267
|
+
return primary[0].accountId;
|
|
1268
|
+
};
|
|
1269
|
+
const from = resolve(opts.from === undefined ? adapter.accountId : opts.from);
|
|
1270
|
+
const to = resolve(destination);
|
|
1271
|
+
for (const id of [from, to]) {
|
|
1272
|
+
if (!accounts.some(a => a.accountId === id)) {
|
|
1273
|
+
throw new PerpError("INVALID_PARAMS", `QFEX account ${id} is not one of your accounts`, { remediation: `Your accounts: ${accounts.map(a => `${a.accountId}${a.isPrimary ? " (primary)" : ""}`).join(", ")}` });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (!isJson())
|
|
1277
|
+
console.log(chalk.cyan(`\n Transferring ${amount} USDC on QFEX: ${from} → ${to}...\n`));
|
|
1278
|
+
const result = await adapter.transferBetweenAccounts(amount, from, to);
|
|
1279
|
+
logExecution({ type: "bridge", exchange: "qfex", symbol: "USDC", side: "transfer", size: amount, status: "success", dryRun: false, meta: { action: "transfer", from, to } });
|
|
1280
|
+
if (isJson())
|
|
1281
|
+
return printJson(jsonOk(result));
|
|
1282
|
+
console.log(chalk.green(" Transfer accepted by QFEX."));
|
|
1283
|
+
console.log(chalk.gray(" Verify both balances: perp account balance -e qfex\n"));
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (opts.from !== undefined || opts.agent !== undefined) {
|
|
1287
|
+
throw new PerpError("INVALID_PARAMS", "--from / --agent apply to QFEX transfers only", { remediation: "Add -e qfex, or drop the flags for a Hyperliquid transfer" });
|
|
1288
|
+
}
|
|
1289
|
+
// usdSend pays another account: the destination must pass settings.withdraw.hyperliquid
|
|
1290
|
+
// (allowlist + maxPerTx / maxPerDay, shared with withdrawals to non-owner addresses).
|
|
1291
|
+
const amountStr = parsePositiveDecimal(amount, "amount");
|
|
1292
|
+
const gateOf = () => {
|
|
1293
|
+
const g = loadSettings().withdraw.hyperliquid;
|
|
1294
|
+
return g === undefined ? null : g;
|
|
1295
|
+
};
|
|
1296
|
+
if (isDryRun()) {
|
|
1297
|
+
// The gate needs no key and no network: a dry run refuses exactly as the live send would.
|
|
1298
|
+
assertWithdrawAllowed("hyperliquid", getNetwork(), amountStr, destination, gateOf(), Date.now());
|
|
1299
|
+
return printFundsPlan({ action: "transfer", venue: "hyperliquid", network: getNetwork(), asset: "USDC", amount: String(amountNum), destination, withdrawGate: "passes (destination allowlisted in settings withdraw.hyperliquid, within maxPerTx / maxPerDay)" });
|
|
1300
|
+
}
|
|
1301
|
+
await requireMaster("hyperliquid", "Hyperliquid transfer");
|
|
1302
|
+
assertWithdrawAllowed("hyperliquid", getNetwork(), amountStr, destination, gateOf(), Date.now());
|
|
1303
|
+
// Not -e qfex → the Hyperliquid transfer (as the dry-run plan says): fetch it directly, independent of -e.
|
|
1304
|
+
const adapter = await getAdapterForExchange("hyperliquid");
|
|
662
1305
|
if (!isUsdTransferCapable(adapter))
|
|
663
|
-
throw new Error("
|
|
1306
|
+
throw new Error("hyperliquid venue did not produce a USD-transfer-capable adapter");
|
|
664
1307
|
if (!isJson())
|
|
665
1308
|
console.log(chalk.cyan(`\n Transferring $${formatUsd(amountNum)} USDC to ${destination}...\n`));
|
|
666
|
-
|
|
1309
|
+
// owner = null: every usdSend destination is gated (sending to another account is the point).
|
|
1310
|
+
const attempt = hyperliquidGateEnforce(getNetwork(), amountStr, destination, null, Date.now());
|
|
1311
|
+
let result;
|
|
1312
|
+
try {
|
|
1313
|
+
result = await adapter.usdTransfer(amountNum, destination);
|
|
1314
|
+
}
|
|
1315
|
+
catch (err) {
|
|
1316
|
+
if (attempt !== null && isDefiniteOrderFailure(err))
|
|
1317
|
+
forgetWithdrawAttempt(attempt);
|
|
1318
|
+
throw err;
|
|
1319
|
+
}
|
|
667
1320
|
if (isJson())
|
|
668
1321
|
return printJson(jsonOk(result));
|
|
669
1322
|
console.log(chalk.green(` Transfer complete!`));
|
|
@@ -685,11 +1338,20 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
685
1338
|
pacifica: { chain: "Solana", token: "USDC", min: 10, gas: "SOL ~0.005", method: "on-chain program" },
|
|
686
1339
|
hyperliquid: { chain: "Arbitrum", token: "USDC", min: 5, gas: "ETH ~0.0001", method: "USDC transfer to Bridge2" },
|
|
687
1340
|
lighter: { chain: "Ethereum L1 / Arbitrum / Base / Avalanche", token: "USDC", min: "1 (L1) or 5 (CCTP)", gas: "$3-10 (L1), ~$0.01 (CCTP)", method: "deposit() or CCTP intent" },
|
|
1341
|
+
"lighter-rh": { chain: "Robinhood Chain", token: "USDG", min: 1, gas: "Robinhood Chain ETH", method: "approve + deposit() (mainnet only)" },
|
|
1342
|
+
qfex: { chain: "Arbitrum", token: "USDC", min: null, method: "address display only: perp funds deposit qfex, then send USDC from your wallet" },
|
|
1343
|
+
aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", min: null, gas: "BNB / Arbitrum ETH", method: "approve + vault depositFor(token, forAddress, amount, broker 1) (mainnet only)", command: "perp funds deposit aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]" },
|
|
1344
|
+
ondo: { chain: "Arbitrum (default) / Ethereum", token: "USDC", min: null, gas: "ETH", method: "USDC transfer to the account's funding address (mainnet only)", command: "perp funds deposit ondo <amount> [--chain arbitrum|ethereum] [--probe <amount>]" },
|
|
688
1345
|
},
|
|
689
1346
|
withdraw: {
|
|
690
1347
|
pacifica: { chain: "Solana", token: "USDC", speed: "~10s", fee: "none" },
|
|
691
1348
|
hyperliquid: { chain: "Arbitrum", token: "USDC", speed: "minutes", fee: "$1" },
|
|
692
1349
|
lighter: { chain: "Ethereum L1", token: "USDC", speed: "~12 hours (standard), minutes (fast)", fee: "varies" },
|
|
1350
|
+
"lighter-rh": { chain: "Robinhood Chain", token: "USDG", command: "perp -e lighter-rh funds withdraw lighter <amount>" },
|
|
1351
|
+
ondo: { supported: false, note: "the Ondo Perps API does not allow withdrawals — withdraw in the Ondo Perps web app" },
|
|
1352
|
+
qfex: { supported: false, note: "QFEX withdrawals are disabled in perp-cli for now — withdraw in the QFEX web app" },
|
|
1353
|
+
arcus: { chain: "Robinhood Chain", token: "USDG", command: "perp funds withdraw arcus <amount>" },
|
|
1354
|
+
aster: { chain: "BNB Chain (default) / Arbitrum", token: "USDT (default) / USDC", command: "perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]", fee: "POST /fapi/v3/aster/user-withdraw-info (taken from the amount)", requires: "mainnet; API wallet approved with --can-withdraw --ip-whitelist; destination = the account's own address" },
|
|
693
1355
|
},
|
|
694
1356
|
}));
|
|
695
1357
|
}
|
|
@@ -714,6 +1376,15 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
714
1376
|
console.log(` L1: ${chalk.green("perp funds deposit lighter ethereum <amount>")} — gas $3-10+`);
|
|
715
1377
|
console.log(` CCTP: ${chalk.green("perp funds deposit lighter cctp <chain> <amount>")} — gas ~$0.01`);
|
|
716
1378
|
console.log(` Chains: arbitrum, base, avalanche`);
|
|
1379
|
+
console.log(chalk.white.bold("\n Lighter RH") + chalk.gray(" (Robinhood Chain)"));
|
|
1380
|
+
console.log(` Token: USDG | Min: 1 | Gas: Robinhood Chain ETH (mainnet only)`);
|
|
1381
|
+
console.log(` Command: ${chalk.green("perp funds deposit lighter-rh <amount>")}`);
|
|
1382
|
+
console.log(chalk.white.bold("\n Aster") + chalk.gray(" (BNB Chain / Arbitrum vault contract)"));
|
|
1383
|
+
console.log(` Token: USDT (default) or USDC | Gas: BNB / Arbitrum ETH (mainnet only)`);
|
|
1384
|
+
console.log(` Command: ${chalk.green("perp funds deposit aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]")}`);
|
|
1385
|
+
console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" (Arbitrum / Ethereum — USDC transfer to your funding address)"));
|
|
1386
|
+
console.log(` Token: USDC | Gas: ETH (mainnet only)`);
|
|
1387
|
+
console.log(` Command: ${chalk.green("perp funds deposit ondo <amount> [--chain arbitrum|ethereum] [--probe <amount>]")}`);
|
|
717
1388
|
// Withdraw info
|
|
718
1389
|
console.log(chalk.white.bold("\n ── Withdraw ──\n"));
|
|
719
1390
|
console.log(chalk.white.bold(" Pacifica") + chalk.gray(" → Solana"));
|
|
@@ -725,6 +1396,15 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
725
1396
|
console.log(chalk.white.bold("\n Lighter") + chalk.gray(" → Ethereum L1"));
|
|
726
1397
|
console.log(` Speed: ~12h (standard) | Fee: varies`);
|
|
727
1398
|
console.log(` Command: ${chalk.green("perp -e lt funds withdraw lighter <amount>")}`);
|
|
1399
|
+
console.log(chalk.white.bold("\n Lighter RH") + chalk.gray(" → Robinhood Chain (USDG)"));
|
|
1400
|
+
console.log(` Command: ${chalk.green("perp -e lighter-rh funds withdraw lighter <amount>")}`);
|
|
1401
|
+
console.log(chalk.white.bold("\n Ondo Perps") + chalk.gray(" — not available over the API"));
|
|
1402
|
+
console.log(chalk.gray(" Withdraw in the Ondo Perps web app."));
|
|
1403
|
+
console.log(chalk.white.bold("\n Arcus") + chalk.gray(" → Robinhood Chain (USDG, to the account owner)"));
|
|
1404
|
+
console.log(` Command: ${chalk.green("perp funds withdraw arcus <amount>")}`);
|
|
1405
|
+
console.log(chalk.white.bold("\n Aster") + chalk.gray(" → BNB Chain / Arbitrum (USDT, to the account's own address; mainnet only)"));
|
|
1406
|
+
console.log(` Needs an API wallet approved with --can-withdraw --ip-whitelist; fee from user-withdraw-info`);
|
|
1407
|
+
console.log(` Command: ${chalk.green("perp funds withdraw aster <amount> [--chain bsc|arbitrum] [--asset USDT|USDC]")}`);
|
|
728
1408
|
// Transfer + Bridge
|
|
729
1409
|
console.log(chalk.white.bold("\n ── Transfer & Bridge ──\n"));
|
|
730
1410
|
console.log(chalk.white.bold(" Internal Transfer") + chalk.gray(" (Hyperliquid → Hyperliquid)"));
|
|
@@ -739,11 +1419,11 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork, g
|
|
|
739
1419
|
console.log(` Check: ${chalk.green("perp funds rebalance check")}`);
|
|
740
1420
|
console.log(` Plan: ${chalk.green("perp funds rebalance plan")}`);
|
|
741
1421
|
console.log(` Execute: ${chalk.green("perp funds rebalance execute --auto-bridge")}`);
|
|
742
|
-
console.log(chalk.gray("\n
|
|
1422
|
+
console.log(chalk.gray("\n Deposits are direct by default (you pay gas). Add --relay to use the gas-sponsoring relayer.\n"));
|
|
743
1423
|
});
|
|
744
1424
|
// ═══════════════════════════════════════════════════════
|
|
745
1425
|
// NESTED SUBTREES — bridge (multi-provider) + rebalance
|
|
746
1426
|
// ═══════════════════════════════════════════════════════
|
|
747
|
-
registerFundsBridgeCommands(funds, isJson);
|
|
748
|
-
registerFundsRebalanceCommands(funds, getAdapterForExchange, isJson);
|
|
1427
|
+
registerFundsBridgeCommands(funds, isJson, getNetwork);
|
|
1428
|
+
registerFundsRebalanceCommands(funds, getAdapterForExchange, isJson, getNetwork);
|
|
749
1429
|
}
|